Bug 1852340 - part 1: Add a new Gecko component for reporting broken websites; r=ayeddi,fluent-reviewers,mossop,flod,Gijs

Differential Revision: https://phabricator.services.mozilla.com/D190647
This commit is contained in:
Thomas Wisniewski 2023-11-20 01:50:36 +00:00
Родитель 482c524980
Коммит 6cb66d4ccd
32 изменённых файлов: 2980 добавлений и 28 удалений

Просмотреть файл

@ -2900,6 +2900,21 @@ pref("cookiebanners.ui.desktop.cfrVariant", 0);
pref("dom.security.credentialmanagement.identity.enabled", true);
#endif
#if defined(NIGHTLY_BUILD)
pref("ui.new-webcompat-reporter.enabled", true);
#else
pref("ui.new-webcompat-reporter.enabled", false);
#endif
#if defined(EARLY_BETA_OR_EARLIER)
pref("ui.new-webcompat-reporter.send-more-info-link", true);
#else
pref("ui.new-webcompat-reporter.send-more-info-link", false);
#endif
# 0 = disabled, 1 = reason optional, 2 = reason required.
pref("ui.new-webcompat-reporter.reason-dropdown", 0);
// Reset Private Browsing Session feature
#if defined(NIGHTLY_BUILD)
pref("browser.privatebrowsing.resetPBM.enabled", true);

Просмотреть файл

@ -146,6 +146,11 @@
data-l10n-id="appmenuitem-more-tools"
closemenu="none"
oncommand="PanelUI.showMoreToolsPanel(this);"/>
<toolbarbutton id="appMenu-report-broken-site-button"
class="subviewbutton subviewbutton-nav"
data-l10n-id="appmenuitem-report-broken-site"
closemenu="none"
command="cmd_reportBrokenSite"/>
<toolbarbutton id="appMenu-help-button2"
class="subviewbutton subviewbutton-nav"
data-l10n-id="appmenuitem-help"

Просмотреть файл

@ -472,6 +472,10 @@
#else
/>
#endif
<menuitem id="help_reportBrokenSite"
command="cmd_reportBrokenSite"
data-l10n-id="menu-report-broken-site"
appmenu-data-l10n-id="menu-report-broken-site"/>
<menuitem id="feedbackPage"
oncommand="openFeedbackPage()"
data-l10n-id="menu-help-share-ideas"

Просмотреть файл

@ -51,6 +51,7 @@
#ifdef XP_MACOSX
<command id="cmd_findSelection" oncommand="gLazyFindCommand('onFindSelectionCommand')"/>
#endif
<command id="cmd_reportBrokenSite" oncommand="ReportBrokenSite.open(event);"/>
<command id="cmd_translate" oncommand="TranslationsPanel.open(event);"/>
<!-- work-around bug 392512 -->
<command id="Browser:AddBookmarkAs"

Просмотреть файл

@ -65,6 +65,7 @@ ChromeUtils.defineESModuleGetters(this, {
PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
ResetPBMPanel: "resource:///modules/ResetPBMPanel.sys.mjs",
ReportBrokenSite: "resource:///modules/ReportBrokenSite.sys.mjs",
SafeBrowsing: "resource://gre/modules/SafeBrowsing.sys.mjs",
Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs",
@ -1879,6 +1880,7 @@ var gBrowserInit = {
// apply full zoom settings to tabs restored by the session restore service.
FullZoom.init();
PanelUI.init(shouldSuppressPopupNotifications);
ReportBrokenSite.init(gBrowser);
UpdateUrlbarSearchSplitterState();

Просмотреть файл

@ -78,6 +78,7 @@
<link rel="localization" href="browser/panelUI.ftl"/>
<link rel="localization" href="browser/places.ftl"/>
<link rel="localization" href="browser/protectionsPanel.ftl"/>
<link rel="localization" href="browser/reportBrokenSite.ftl"/>
<link rel="localization" href="browser/screenshots.ftl"/>
<link rel="localization" href="browser/search.ftl"/>
<link rel="localization" href="browser/sidebarMenu.ftl"/>

Просмотреть файл

@ -14,6 +14,7 @@
<html:link rel="localization" href="browser/browserSets.ftl"/>
<html:link rel="localization" href="browser/firefoxView.ftl"/>
<html:link rel="localization" href="browser/menubar.ftl"/>
<html:link rel="localization" href="browser/reportBrokenSite.ftl"/>
<html:link rel="localization" href="browser/screenshots.ftl"/>
<html:link rel="localization" href="toolkit/branding/accounts.ftl"/>
<html:link rel="localization" href="toolkit/branding/brandings.ftl"/>

Просмотреть файл

@ -500,6 +500,7 @@
#include ../../components/controlcenter/content/protectionsPanel.inc.xhtml
#include ../../components/downloads/content/downloadsPanel.inc.xhtml
#include ../../components/translations/content/translationsPanel.inc.xhtml
#include ../../components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml
#include browser-allTabsMenu.inc.xhtml
<tooltip id="dynamic-shortcut-tooltip"

Просмотреть файл

@ -286,9 +286,6 @@ var allowlist = [
{ file: "chrome://browser/content/screenshots/copy.svg" },
{ file: "chrome://browser/content/screenshots/download.svg" },
{ file: "chrome://browser/content/screenshots/download-white.svg" },
// Bug 1852340: adding strings early for new broken website reporting component
{ file: "resource://app/localization/en-US/browser/reportBrokenSite.ftl" },
];
if (AppConstants.NIGHTLY_BUILD && AppConstants.platform != "win") {

Просмотреть файл

@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(this, {
ExtensionSettingsStore:
"resource://gre/modules/ExtensionSettingsStore.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ReportBrokenSite: "resource:///modules/ReportBrokenSite.sys.mjs",
ShellService: "resource:///modules/ShellService.sys.mjs",
URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
});
@ -540,30 +541,6 @@ function buildHelpMenu() {
gSafeBrowsing.setReportPhishingMenu();
}
// We're testing to see if the WebCompat team's "Report Site Issue"
// access point makes sense in the Help menu. Normally checking this
// pref wouldn't be enough, since there's also the case that the
// add-on has somehow been disabled by the user or third-party software
// without flipping the pref. Since this add-on is only used on pre-release
// channels, and since the jury is still out on whether or not the Help menu
// is the right place for this item, we're going to do a least-effort
// approach here and assume that the pref is enough to determine whether the
// menuitem should appear.
//
// See bug 1690573 for further details.
let reportSiteIssueEnabled = Services.prefs.getBoolPref(
"extensions.webcompat-reporter.enabled",
false
);
let reportSiteIssue = document.getElementById("help_reportSiteIssue");
reportSiteIssue.hidden = !reportSiteIssueEnabled;
if (reportSiteIssueEnabled) {
let uri = gBrowser.currentURI;
let isReportablePage =
uri && (uri.schemeIs("http") || uri.schemeIs("https"));
reportSiteIssue.disabled = !isReportablePage;
}
if (NimbusFeatures.deviceMigration.getVariable("helpMenuHidden")) {
let helpMenuItem = document.getElementById("helpSwitchDevice");
helpMenuItem.hidden = true;

Просмотреть файл

@ -9,7 +9,7 @@
role="alertdialog"
noautofocus="true"
aria-labelledby="protections-popup-main-header-label"
onpopupshown="gProtectionsHandler.onPopupShown(event);"
onpopupshown="gProtectionsHandler.onPopupShown(event);ReportBrokenSite.updateParentMenu(event)"
onpopuphidden="gProtectionsHandler.onPopupHidden(event);"
orient="vertical">
@ -63,6 +63,13 @@
</hbox>
</vbox>
<toolbarseparator observes="cmd_reportBrokenSite"></toolbarseparator>
<toolbarbutton id="protections-popup-report-broken-site-button"
command="cmd_reportBrokenSite"
class="subviewbutton subviewbutton-nav"
data-l10n-id="protections-panel-report-broken-site"
closemenu="none"/>
<!-- Tracking Protection Section -->
<toolbarseparator></toolbarseparator>
<vbox id="tracking-protection-container" class="protections-popup-section">

Просмотреть файл

@ -51,6 +51,7 @@ DIRS += [
"prompts",
"protections",
"protocolhandler",
"reportbrokensite",
"resistfingerprinting",
"screenshots",
"search",

Просмотреть файл

@ -0,0 +1,566 @@
/* 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/browser-window */
const DEFAULT_NEW_REPORT_ENDPOINT = "https://webcompat.com/issues/new";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const gDescriptionCheckRE = /\S/;
class ViewState {
#doc;
#mainView;
#reportSentView;
currentTabURI;
currentTabWebcompatDetailsPromise;
isURLValid = false;
isDescriptionValid = false;
isReasonValid = false;
constructor(doc) {
this.#doc = doc;
this.#mainView = doc.ownerGlobal.PanelMultiView.getViewNode(
this.#doc,
"report-broken-site-popup-mainView"
);
this.#reportSentView = doc.ownerGlobal.PanelMultiView.getViewNode(
this.#doc,
"report-broken-site-popup-reportSentView"
);
ViewState.#cache.set(doc, this);
}
static #cache = new WeakMap();
static get(doc) {
return ViewState.#cache.get(doc) ?? new ViewState(doc);
}
get urlInput() {
return this.#mainView.querySelector("#report-broken-site-popup-url");
}
get url() {
return this.urlInput.value;
}
set url(spec) {
this.urlInput.value = spec;
}
resetURLToCurrentTab() {
const { currentURI } = this.#doc.ownerGlobal.gBrowser.selectedBrowser;
this.currentTabURI = currentURI;
this.urlInput.value = currentURI.spec;
this.isURLValid = true;
}
get descriptionInput() {
return this.#mainView.querySelector(
"#report-broken-site-popup-description"
);
}
get description() {
return this.descriptionInput.value;
}
set description(value) {
this.descriptionInput.value = value;
}
static REASON_CHOICES_ID_PREFIX = "report-broken-site-popup-reason-";
get reasonInput() {
return this.#mainView.querySelector("#report-broken-site-popup-reason");
}
get reason() {
const reason = this.reasonInput.selectedItem.id.replace(
ViewState.REASON_CHOICES_ID_PREFIX,
""
);
return reason == "choose" ? undefined : reason;
}
set reason(value) {
this.reasonInput.selectedItem = this.#mainView.querySelector(
`#${ViewState.REASON_CHOICES_ID_PREFIX}${value}`
);
}
static CHOOSE_A_REASON_OPT_ID = "report-broken-site-popup-reason-choose";
get chooseAReasonOption() {
return this.#mainView.querySelector(`#${ViewState.CHOOSE_A_REASON_OPT_ID}`);
}
reset() {
this.currentTabWebcompatDetailsPromise = undefined;
this.resetURLToCurrentTab();
this.description = "";
this.isDescriptionValid = false;
this.reason = "choose";
this.isReasonValid = false;
}
enableOrDisableSendButton(
isReasonEnabled,
isReasonOptional,
isDescriptionOptional
) {
const isReasonOkay =
!isReasonEnabled || isReasonOptional || this.isReasonValid;
const isDescriptionOkay = isDescriptionOptional || this.isDescriptionValid;
this.sendButton.disabled =
!this.isURLValid || !isReasonOkay || !isDescriptionOkay;
}
get sendMoreInfoLink() {
return this.#mainView.querySelector(
"#report-broken-site-popup-send-more-info-link"
);
}
get reasonLabelRequired() {
return this.#mainView.querySelector(
"#report-broken-site-popup-reason-label"
);
}
get reasonLabelOptional() {
return this.#mainView.querySelector(
"#report-broken-site-popup-reason-optional-label"
);
}
get descriptionLabelRequired() {
return this.#mainView.querySelector(
"#report-broken-site-popup-description-label"
);
}
get descriptionLabelOptional() {
return this.#mainView.querySelector(
"#report-broken-site-popup-description-optional-label"
);
}
get sendButton() {
return this.#mainView.querySelector(
"#report-broken-site-popup-send-button"
);
}
get cancelButton() {
return this.#mainView.querySelector(
"#report-broken-site-popup-cancel-button"
);
}
get mainView() {
return this.#mainView;
}
get reportSentView() {
return this.#reportSentView;
}
get okayButton() {
return this.#reportSentView.querySelector(
"#report-broken-site-popup-okay-button"
);
}
}
export var ReportBrokenSite = new (class ReportBrokenSite {
#newReportEndpoint = undefined;
get sendMoreInfoEndpoint() {
return this.#newReportEndpoint || DEFAULT_NEW_REPORT_ENDPOINT;
}
static WEBCOMPAT_REPORTER_CONFIG = {
src: "desktop-reporter",
utm_campaign: "report-broken-site",
utm_source: "desktop-reporter",
};
static DATAREPORTING_PREF = "datareporting.healthreport.uploadEnabled";
static REPORTER_ENABLED_PREF = "ui.new-webcompat-reporter.enabled";
static REASON_PREF = "ui.new-webcompat-reporter.reason-dropdown";
static SEND_MORE_INFO_PREF = "ui.new-webcompat-reporter.send-more-info-link";
static NEW_REPORT_ENDPOINT_PREF =
"ui.new-webcompat-reporter.new-report-endpoint";
static REPORT_SITE_ISSUE_PREF = "extensions.webcompat-reporter.enabled";
static MAIN_PANELVIEW_ID = "report-broken-site-popup-mainView";
static SENT_PANELVIEW_ID = "report-broken-site-popup-reportSentView";
#_enabled = false;
get enabled() {
return this.#_enabled;
}
#reasonEnabled = false;
#reasonIsOptional = true;
#descriptionIsOptional = true;
#sendMoreInfoEnabled = true;
constructor() {
for (const [name, [pref, dflt]] of Object.entries({
dataReportingPref: [ReportBrokenSite.DATAREPORTING_PREF, false],
reasonPref: [ReportBrokenSite.REASON_PREF, 0],
sendMoreInfoPref: [ReportBrokenSite.SEND_MORE_INFO_PREF, false],
newReportEndpointPref: [
ReportBrokenSite.NEW_REPORT_ENDPOINT_PREF,
DEFAULT_NEW_REPORT_ENDPOINT,
],
enabledPref: [ReportBrokenSite.REPORTER_ENABLED_PREF, true],
reportSiteIssueEnabledPref: [
ReportBrokenSite.REPORT_SITE_ISSUE_PREF,
false,
],
})) {
XPCOMUtils.defineLazyPreferenceGetter(
this,
name,
pref,
dflt,
this.#checkPrefs.bind(this)
);
}
this.#checkPrefs();
}
canReportURI(uri) {
return uri && (uri.schemeIs("http") || uri.schemeIs("https"));
}
updateParentMenu(event) {
// We need to make sure that the Report Broken Site menu item
// is disabled and/or hidden depending on the prefs/active tab URL
// when our parent popups are shown, and if their tab's location
// changes while they are open.
const tabbrowser = event.target.ownerGlobal.gBrowser;
this.enableOrDisableMenuitems(tabbrowser.selectedBrowser);
tabbrowser.addTabsProgressListener(this);
event.target.addEventListener(
"popuphidden",
() => {
tabbrowser.removeTabsProgressListener(this);
},
{ once: true }
);
}
init(tabbrowser) {
// Called in browser.js.
const { ownerGlobal } = tabbrowser.selectedBrowser;
const { document } = ownerGlobal;
const state = ViewState.get(document);
this.#initMainView(state);
this.#initReportSentView(state);
for (const id of ["menu_HelpPopup", "appMenu-popup"]) {
document
.getElementById(id)
.addEventListener("popupshown", this.updateParentMenu.bind(this));
}
ownerGlobal.PanelMultiView.getViewNode(
document,
ReportBrokenSite.MAIN_PANELVIEW_ID
).addEventListener("ViewShowing", ({ target }) => {
const { selectedBrowser } = target.ownerGlobal.gBrowser;
let source = "helpMenu";
switch (target.closest("panelmultiview")?.id) {
case "appMenu-multiView":
source = "hamburgerMenu";
break;
case "protections-popup-multiView":
source = "ETPShieldIconMenu";
break;
}
this.#onMainViewShown(source, selectedBrowser);
});
}
enableOrDisableMenuitems(selectedbrowser) {
// Ensures that the various Report Broken Site menu items and
// toolbar buttons are enabled/hidden when appropriate (and
// also the Help menu's Report Site Issue item)/
const canReportUrl = this.canReportURI(selectedbrowser.currentURI);
const { document } = selectedbrowser.ownerGlobal;
const cmd = document.getElementById("cmd_reportBrokenSite");
if (this.enabled) {
cmd.setAttribute("hidden", "false"); // see bug 805653
} else {
cmd.setAttribute("hidden", "true");
}
if (canReportUrl) {
cmd.removeAttribute("disabled");
} else {
cmd.setAttribute("disabled", "true");
}
// Changes to the "hidden" and "disabled" state of the command aren't reliably
// reflected on the main menu unless we open it twice, or do it manually.
// (See bug 1864953).
const mainmenuItem = document.getElementById("help_reportBrokenSite");
if (mainmenuItem) {
mainmenuItem.hidden = !this.enabled;
mainmenuItem.disabled = !canReportUrl;
}
// Report Site Issue is our older issue reporter, shown in the Help
// menu on pre-release channels. We should hide it unless we're
// disabled, at which point we should show it when available.
const reportSiteIssue = document.getElementById("help_reportSiteIssue");
if (reportSiteIssue) {
reportSiteIssue.hidden = this.enabled || !this.reportSiteIssueEnabledPref;
reportSiteIssue.disabled = !canReportUrl;
}
}
#checkPrefs(whichChanged) {
// No breakage reports can be sent by Glean if it's disabled, so we also
// disable the broken site reporter. We also have our own pref.
this.#_enabled =
Services.policies.isAllowed("feedbackCommands") &&
this.dataReportingPref &&
this.enabledPref;
this.#reasonEnabled = this.reasonPref == 1 || this.reasonPref == 2;
this.#reasonIsOptional = this.reasonPref == 1;
this.#sendMoreInfoEnabled = this.sendMoreInfoPref;
this.#newReportEndpoint = this.newReportEndpointPref;
}
#initMainView(state) {
state.sendButton.addEventListener("command", async ({ target }) => {
const multiview = target.closest("panelmultiview");
state.reportSentView.hidden = false;
multiview.showSubView("report-broken-site-popup-reportSentView");
state.reset();
});
state.cancelButton.addEventListener("command", ({ target, view }) => {
const multiview = target.closest("panelmultiview");
view.ownerGlobal.PanelMultiView.forNode(multiview).hidePopup();
state.reset();
});
state.sendMoreInfoLink.addEventListener("click", async event => {
event.preventDefault();
const tabbrowser = event.view.ownerGlobal.gBrowser;
const multiview = event.target.closest("panelmultiview");
event.view.ownerGlobal.PanelMultiView.forNode(multiview).hidePopup();
await this.#openWebCompatTab(tabbrowser);
state.reset();
});
state.urlInput.addEventListener("input", ({ target }) => {
const newUrlValid = target.value && target.checkValidity();
if (state.isURLValid != newUrlValid) {
state.isURLValid = newUrlValid;
state.enableOrDisableSendButton(
this.#reasonEnabled,
this.#reasonIsOptional,
this.#descriptionIsOptional
);
}
});
state.descriptionInput.addEventListener("input", ({ target }) => {
const newDescValid = gDescriptionCheckRE.test(target.value);
if (state.isDescriptionValid != newDescValid) {
state.isDescriptionValid = newDescValid;
state.enableOrDisableSendButton(
this.#reasonEnabled,
this.#reasonIsOptional,
this.#descriptionIsOptional
);
}
});
const reasonDropdown = state.reasonInput;
reasonDropdown.addEventListener("command", ({ target }) => {
const choiceId = target.closest("menulist").selectedItem.id;
const newValidity = choiceId !== ViewState.CHOOSE_A_REASON_OPT_ID;
if (state.isReasonValid != newValidity) {
state.isReasonValid = newValidity;
state.enableOrDisableSendButton(
this.#reasonEnabled,
this.#reasonIsOptional,
this.#descriptionIsOptional
);
}
});
const menupopup = reasonDropdown.querySelector("menupopup");
const onDropDownShowOrHide = ({ type }) => {
// Hide "choose a reason" while the user has the reason dropdown open
const shouldHide = type == "popupshowing";
state.chooseAReasonOption.hidden = shouldHide;
};
menupopup.addEventListener("popupshowing", onDropDownShowOrHide);
menupopup.addEventListener("popuphiding", onDropDownShowOrHide);
}
#initReportSentView(state) {
state.okayButton.addEventListener("command", ({ target, view }) => {
const multiview = target.closest("panelmultiview");
view.ownerGlobal.PanelMultiView.forNode(multiview).hidePopup();
});
}
async #onMainViewShown(source, selectedBrowser) {
const { document } = selectedBrowser.ownerGlobal;
let didReset = false;
const state = ViewState.get(document);
const uri = selectedBrowser.currentURI;
if (!state.isURLValid && !state.isDescriptionValid) {
state.reset();
didReset = true;
} else if (!state.currentTabURI || !uri.equals(state.currentTabURI)) {
state.reset();
didReset = true;
} else if (!state.url) {
state.resetURLToCurrentTab();
}
state.mainView.hidden = false;
state.sendMoreInfoLink.hidden = !this.#sendMoreInfoEnabled;
state.reasonInput.hidden = !this.#reasonEnabled;
state.reasonLabelRequired.hidden =
!this.#reasonEnabled || this.#reasonIsOptional;
state.reasonLabelOptional.hidden =
!this.#reasonEnabled || !this.#reasonIsOptional;
state.descriptionLabelRequired.hidden = this.#descriptionIsOptional;
state.descriptionLabelOptional.hidden = !this.#descriptionIsOptional;
state.enableOrDisableSendButton(
this.#reasonEnabled,
this.#reasonIsOptional,
this.#descriptionIsOptional
);
if (didReset || !state.currentTabWebcompatDetailsPromise) {
state.currentTabWebcompatDetailsPromise = this.#queryActor(
"GetWebCompatInfo",
undefined,
selectedBrowser
).catch(err => {
console.error("Report Broken Site: unexpected error", err);
});
}
}
async #queryActor(msg, params, browser) {
const actor =
browser.browsingContext.currentWindowGlobal.getActor("ReportBrokenSite");
return actor.sendQuery(msg, params);
}
async #loadTab(tabbrowser, url, triggeringPrincipal) {
const tab = tabbrowser.addTab(url, {
inBackground: false,
triggeringPrincipal,
});
const expectedBrowser = tabbrowser.getBrowserForTab(tab);
return new Promise(resolve => {
const listener = {
onLocationChange(browser, webProgress, request, uri, flags) {
if (
browser == expectedBrowser &&
uri.spec == url &&
webProgress.isTopLevel
) {
resolve(tab);
tabbrowser.removeTabsProgressListener(listener);
}
},
};
tabbrowser.addTabsProgressListener(listener);
});
}
async #openWebCompatTab(tabbrowser) {
const endpointUrl = this.sendMoreInfoEndpoint;
const principal = Services.scriptSecurityManager.createNullPrincipal({});
const tab = await this.#loadTab(tabbrowser, endpointUrl, principal);
const { document } = tabbrowser.selectedBrowser.ownerGlobal;
const { description, reason, url, currentTabWebcompatDetailsPromise } =
ViewState.get(document);
return this.#queryActor(
"SendDataToWebcompatCom",
{
reason,
description,
endpointUrl,
reportUrl: url,
reporterConfig: ReportBrokenSite.WEBCOMPAT_REPORTER_CONFIG,
webcompatInfo: await currentTabWebcompatDetailsPromise,
},
tab.linkedBrowser
).catch(err => {
console.error("Report Broken Site: unexpected error", err);
});
}
open(event) {
const { target } = event.sourceEvent;
const { selectedBrowser } = event.view.ownerGlobal.gBrowser;
const { ownerGlobal } = selectedBrowser;
const { document } = ownerGlobal;
switch (target.id) {
case "appMenu-report-broken-site-button":
ownerGlobal.PanelUI.showSubView(
ReportBrokenSite.MAIN_PANELVIEW_ID,
target
);
break;
case "protections-popup-report-broken-site-button":
document
.getElementById("protections-popup-multiView")
.showSubView(ReportBrokenSite.MAIN_PANELVIEW_ID);
break;
case "help_reportBrokenSite":
// hide the hamburger menu first, as we overlap with it.
const appMenuPopup = document.getElementById("appMenu-popup");
appMenuPopup?.hidePopup();
// See bug 1864957; we should be able to use showSubView here
ownerGlobal.PanelMultiView.openPopup(
document.getElementById("report-broken-main-menu-panel"),
document.getElementById("PanelUI-menu-button"),
{ position: "bottomright topright" }
);
break;
}
}
})();

Просмотреть файл

@ -0,0 +1,120 @@
<!-- 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/. -->
<panel id="report-broken-main-menu-panel"
class="cui-widget-panel panel-no-padding"
type="arrow"
role="document"
noautofocus="true"
tabspecific="true"
flip="slide"
orient="vertical">
<panelmultiview mainViewId="report-broken-site-popup-mainView"/>
</panel>
<panelview id="report-broken-site-popup-mainView"
class="report-broken-site-view main-view PanelUI-subView"
role="document"
data-l10n-id="report-broken-site-panel-header"
data-l10n-attrs="title"
hidden="true"
showheader="true">
<!-- We need to have a panel-header for the main/help menu, and some element
inside of the header needs a .panel-info-button class for the back button
to be shown when opened from the appmenu or protections panel, as per
https://searchfox.org/mozilla-central/source/browser/components/customizableui/PanelMultiView.sys.mjs#1423
-->
<box class="panel-header">
<html:h1 role="heading" aria-level="1" class="panel-info-button">
<html:span data-l10n-id="report-broken-site-mainview-title"/>
</html:h1>
</box>
<toolbarseparator></toolbarseparator>
<vbox id="report-broken-site-panel-container"
role="alertdialog"
aria-labelledby="report-broken-site-panel-intro">
<html:p data-l10n-id="report-broken-site-panel-intro"/>
<label id="report-broken-site-popup-url-label"
control="report-broken-site-popup-url"
data-l10n-id="report-broken-site-panel-url"/>
<html:input id="report-broken-site-popup-url"
allow-arrow-navigation="true"
aria-describedby="report-broken-site-panel-intro"
type="url"/>
<label id="report-broken-site-popup-reason-label"
control="report-broken-site-popup-reason"
hidden="true"
data-l10n-attrs="aria-label"
data-l10n-id="report-broken-site-panel-reason-label"/>
<label id="report-broken-site-popup-reason-optional-label"
control="report-broken-site-popup-reason"
hidden="true"
data-l10n-attrs="aria-label"
data-l10n-id="report-broken-site-panel-reason-optional-label"/>
<menulist id="report-broken-site-popup-reason"
allow-arrow-navigation="true"
class="plain">
<menupopup>
<menuitem id="report-broken-site-popup-reason-choose"
data-l10n-id="report-broken-site-panel-reason-choose"/>
<menuitem id="report-broken-site-popup-reason-slow"
data-l10n-id="report-broken-site-panel-reason-slow"/>
<menuitem id="report-broken-site-popup-reason-media"
data-l10n-id="report-broken-site-panel-reason-media"/>
<menuitem id="report-broken-site-popup-reason-content"
data-l10n-id="report-broken-site-panel-reason-content"/>
<menuitem id="report-broken-site-popup-reason-account"
data-l10n-id="report-broken-site-panel-reason-account"/>
<menuitem id="report-broken-site-popup-reason-adblockers"
data-l10n-id="report-broken-site-panel-reason-adblockers"/>
<menuitem id="report-broken-site-popup-reason-other"
data-l10n-id="report-broken-site-panel-reason-other"/>
</menupopup>
</menulist>
<label id="report-broken-site-popup-description-label"
control="report-broken-site-popup-description"
hidden="true"
data-l10n-id="report-broken-site-panel-description-label"/>
<label id="report-broken-site-popup-description-optional-label"
control="report-broken-site-popup-description"
hidden="true"
data-l10n-id="report-broken-site-panel-description-optional-label"/>
<html:textarea id="report-broken-site-popup-description"
allow-arrow-navigation="true"
rows="8"></html:textarea>
<html:a id="report-broken-site-popup-send-more-info-link"
href="#"
data-l10n-id="report-broken-site-panel-send-more-info-link"/>
<html:moz-button-group class="panel-footer">
<button id="report-broken-site-popup-cancel-button"
class="footer-button"
data-l10n-id="report-broken-site-panel-button-cancel"/>
<button id="report-broken-site-popup-send-button"
class="footer-button primary"
data-l10n-id="report-broken-site-panel-button-send"/>
</html:moz-button-group>
</vbox>
</panelview>
<panelview id="report-broken-site-popup-reportSentView"
class="report-broken-site-view sent-view PanelUI-subView"
role="dialog"
data-l10n-id="report-broken-site-panel-report-sent-header"
data-l10n-attrs="title"
hidden="true"
showheader="true">
<box class="panel-header">
<html:h1 role="alert" aria-level="1">
<html:span data-l10n-id="report-broken-site-panel-report-sent-label"/>
</html:h1>
</box>
<vbox class="panel-subview-body subview-subheader">
<html:p data-l10n-id="report-broken-site-panel-report-sent-text"/>
<html:moz-button-group class="panel-footer">
<button id="report-broken-site-popup-okay-button"
class="footer-button primary"
data-l10n-id="report-broken-site-panel-button-okay"/>
</html:moz-button-group>
</vbox>
</panelview>

Просмотреть файл

@ -0,0 +1,14 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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_CHROME_MANIFESTS += ["test/browser/browser.toml"]
EXTRA_JS_MODULES += [
"ReportBrokenSite.sys.mjs",
]
with Files("**"):
BUG_COMPONENT = ("Web Compatibility", "Desktop")

Просмотреть файл

@ -0,0 +1,18 @@
[DEFAULT]
tags = "report-broken-site"
support-files = [
"head.js",
"sendMoreInfoTestEndpoint.html",
]
["browser_parent_menuitems.js"]
["browser_reason_dropdown.js"]
["browser_report_send.js"]
["browser_report_site_issue_fallback.js"]
["browser_send_more_info.js"]
["browser_tab_switch_handling.js"]

Просмотреть файл

@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Test that the Report Broken Site menu items are disabled
* when the active tab is not on a reportable URL, and is hidden
* when the feature is disabled via pref.
*/
"use strict";
add_common_setup();
add_task(async function test() {
ensureReportBrokenSitePreffedOff();
const appMenu = AppMenu();
const menus = [appMenu, ProtectionsPanel(), HelpMenu()];
async function forceMenuItemStateUpdate() {
window.ReportBrokenSite.enableOrDisableMenuitems(window);
// the hidden/disabled state of all of the menuitems may not update until one
// is rendered; then the related <command>'s state is propagated to them all.
await appMenu.open();
await appMenu.close();
}
await BrowserTestUtils.withNewTab("about:blank", async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemHidden(
reportBrokenSite,
`${menuDescription} option hidden on invalid page when preffed off`
);
}
});
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemHidden(
reportBrokenSite,
`${menuDescription} option hidden on valid page when preffed off`
);
}
});
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab("about:blank", async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemDisabled(
reportBrokenSite,
`${menuDescription} option disabled on invalid page when preffed on`
);
}
});
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemEnabled(
reportBrokenSite,
`${menuDescription} option enabled on valid page when preffed on`
);
}
});
ensureReportBrokenSitePreffedOff();
await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () {
await forceMenuItemStateUpdate();
for (const { menuDescription, reportBrokenSite } of menus) {
isMenuItemHidden(
reportBrokenSite,
`${menuDescription} option hidden again when pref toggled back off`
);
}
});
});

Просмотреть файл

@ -0,0 +1,65 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that the reason dropdown is shown or hidden
* based on its pref, and that its optional and required modes affect
* the Send button and report appropriately.
*/
"use strict";
add_common_setup();
async function clickSendAndCheckPing(rbs, expectedReason = null) {
const pingCheck = new Promise(resolve => {
GleanPings.brokenSiteReport.testBeforeNextSubmit(() => {
Assert.equal(
Glean.brokenSiteReport.breakageCategory.testGetValue(),
expectedReason
);
resolve();
});
});
await rbs.clickSend();
return pingCheck;
}
add_task(async function testReasonDropdown() {
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab(
REPORTABLE_PAGE_URL,
async function (browser) {
ensureReasonDisabled();
let rbs = await AppMenu().openReportBrokenSite();
await rbs.isReasonHidden();
await rbs.isSendButtonEnabled();
await clickSendAndCheckPing(rbs);
await rbs.clickOkay();
ensureReasonOptional();
rbs = await AppMenu().openReportBrokenSite();
await rbs.isReasonOptional();
await rbs.isSendButtonEnabled();
await clickSendAndCheckPing(rbs);
await rbs.clickOkay();
rbs = await AppMenu().openReportBrokenSite();
await rbs.isReasonOptional();
rbs.chooseReason("slow");
await rbs.isSendButtonEnabled();
await clickSendAndCheckPing(rbs, "slow");
await rbs.clickOkay();
ensureReasonRequired();
rbs = await AppMenu().openReportBrokenSite();
await rbs.isReasonRequired();
await rbs.isSendButtonDisabled();
rbs.chooseReason("media");
await rbs.isSendButtonEnabled();
await clickSendAndCheckPing(rbs, "media");
await rbs.clickOkay();
}
);
});

Просмотреть файл

@ -0,0 +1,107 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that sending or canceling reports with
* the Send and Cancel buttons work (as well as the Okay button)
*/
"use strict";
add_common_setup();
requestLongerTimeout(10);
async function testSend(menu, url, description = "any") {
let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
if (!url) {
url = menu.win.gBrowser.currentURI.spec;
}
const pingCheck = new Promise(resolve => {
GleanPings.brokenSiteReport.testBeforeNextSubmit(() => {
Assert.equal(Glean.brokenSiteReport.url.testGetValue(), url);
Assert.equal(
Glean.brokenSiteReport.description.testGetValue(),
description
);
resolve();
});
});
await rbs.clickSend();
await pingCheck;
await rbs.clickOkay();
// re-opening the panel, the url and description should be reset
rbs = await menu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
}
async function testCancel(menu, url, description) {
let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
await rbs.clickCancel();
ok(!rbs.opened, "clicking Cancel closes Report Broken Site");
// re-opening the panel, the url and description should be reset
rbs = await menu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
}
add_task(async function testSendButton() {
ensureReportBrokenSitePreffedOn();
const tab1 = await openTab(REPORTABLE_PAGE_URL);
await testSend(AppMenu());
const tab2 = await openTab(REPORTABLE_PAGE_URL);
await testSend(
ProtectionsPanel(),
"https://test.org/test/#fake",
"test description"
);
const win2 = await BrowserTestUtils.openNewBrowserWindow();
const tab3 = await openTab(REPORTABLE_PAGE_URL2, win2);
await testSend(AppMenu(win2), null, "another test description");
closeTab(tab3);
await BrowserTestUtils.closeWindow(win2);
closeTab(tab1);
closeTab(tab2);
});
add_task(async function testCancelButton() {
ensureReportBrokenSitePreffedOn();
const tab1 = await openTab(REPORTABLE_PAGE_URL);
await testCancel(AppMenu());
await testCancel(ProtectionsPanel());
await testCancel(HelpMenu());
const tab2 = await openTab(REPORTABLE_PAGE_URL);
await testCancel(AppMenu());
await testCancel(ProtectionsPanel());
await testCancel(HelpMenu());
const win2 = await BrowserTestUtils.openNewBrowserWindow();
const tab3 = await openTab(REPORTABLE_PAGE_URL2, win2);
await testCancel(AppMenu(win2));
await testCancel(ProtectionsPanel(win2));
await testCancel(HelpMenu(win2));
closeTab(tab3);
await BrowserTestUtils.closeWindow(win2);
closeTab(tab1);
closeTab(tab2);
});

Просмотреть файл

@ -0,0 +1,89 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests that when Report Broken Site is active,
* Report Site Issue is hidden.
*/
"use strict";
add_common_setup();
async function testDisabledByReportBrokenSite(menu) {
ensureReportBrokenSitePreffedOn();
ensureReportSiteIssuePreffedOn();
await menu.open();
menu.isReportSiteIssueHidden();
await menu.close();
}
async function testDisabledByPref(menu) {
ensureReportBrokenSitePreffedOff();
ensureReportSiteIssuePreffedOff();
await menu.open();
menu.isReportSiteIssueHidden();
await menu.close();
}
async function testDisabledForInvalidURLs(menu) {
ensureReportBrokenSitePreffedOff();
ensureReportSiteIssuePreffedOn();
await menu.open();
menu.isReportSiteIssueDisabled();
await menu.close();
}
async function testEnabledForValidURLs(menu) {
ensureReportBrokenSitePreffedOff();
ensureReportSiteIssuePreffedOn();
await BrowserTestUtils.withNewTab(
REPORTABLE_PAGE_URL,
async function (browser) {
await menu.open();
menu.isReportSiteIssueEnabled();
await menu.close();
}
);
}
// AppMenu help sub-menu
add_task(async function testDisabledByReportBrokenSiteAppMenuHelpSubmenu() {
await testDisabledByReportBrokenSite(AppMenuHelpSubmenu());
});
// disabled for now due to bug 1775402
//add_task(async function testDisabledByPrefAppMenuHelpSubmenu() {
// await testDisabledByPref(AppMenuHelpSubmenu());
//});
add_task(async function testDisabledForInvalidURLsAppMenuHelpSubmenu() {
await testDisabledForInvalidURLs(AppMenuHelpSubmenu());
});
add_task(async function testEnabledForValidURLsAppMenuHelpSubmenu() {
await testEnabledForValidURLs(AppMenuHelpSubmenu());
});
// Help menu
add_task(async function testDisabledByReportBrokenSiteHelpMenu() {
await testDisabledByReportBrokenSite(HelpMenu());
});
// disabled for now due to bug 1775402
//add_task(async function testDisabledByPrefHelpMenu() {
// await testDisabledByPref(HelpMenu());
//});
add_task(async function testDisabledForInvalidURLsHelpMenu() {
await testDisabledForInvalidURLs(HelpMenu());
});
add_task(async function testEnabledForValidURLsHelpMenu() {
await testEnabledForValidURLs(HelpMenu());
});

Просмотреть файл

@ -0,0 +1,72 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests that the send more info link appears only when its pref
* is set to true, and that when clicked it will open a tab to
* the webcompat.com endpoint and send the right data.
*/
"use strict";
add_common_setup();
add_task(async function testSendMoreInfoPref() {
ensureReportBrokenSitePreffedOn();
await BrowserTestUtils.withNewTab(
REPORTABLE_PAGE_URL,
async function (browser) {
await changeTab(gBrowser.selectedTab, REPORTABLE_PAGE_URL);
ensureSendMoreInfoDisabled();
let rbs = await AppMenu().openReportBrokenSite();
await rbs.isSendMoreInfoHidden();
await rbs.close();
ensureSendMoreInfoEnabled();
rbs = await AppMenu().openReportBrokenSite();
await rbs.isSendMoreInfoShown();
await rbs.close();
}
);
});
async function testSendMoreInfo(menu, url, description = "any") {
let rbs = await menu.openAndPrefillReportBrokenSite(url, description);
if (!url) {
url = menu.win.gBrowser.currentURI.spec;
}
const receivedData = await rbs.clickSendMoreInfo();
const { message } = receivedData;
is(message.url, url, "sent correct URL");
is(message.description, description, "sent correct description");
is(message.src, "desktop-reporter", "sent correct src");
is(message.utm_campaign, "report-broken-site", "sent correct utm_campaign");
is(message.utm_source, "desktop-reporter", "sent correct utm_source");
ok(typeof message.details == "object", "sent extra details");
// re-opening the panel, the url and description should be reset
rbs = await menu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
}
add_task(async function testSendingMoreInfo() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
const tab = await openTab(REPORTABLE_PAGE_URL);
await testSendMoreInfo(AppMenu());
await changeTab(tab, REPORTABLE_PAGE_URL2);
await testSendMoreInfo(ProtectionsPanel(), "https://override.com", "another");
closeTab(tab);
});

Просмотреть файл

@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* Tests to ensure that Report Broken Site popups will be
* reset to whichever tab the user is on as they change
* between windows and tabs. */
"use strict";
add_common_setup();
add_task(async function testResetsProperlyOnTabSwitch() {
ensureReportBrokenSitePreffedOn();
const badTab = await openTab("about:blank");
const goodTab1 = await openTab(REPORTABLE_PAGE_URL);
const goodTab2 = await openTab(REPORTABLE_PAGE_URL2);
const appMenu = AppMenu();
const protPanel = ProtectionsPanel();
let rbs = await appMenu.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
gBrowser.selectedTab = goodTab1;
rbs = await protPanel.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
gBrowser.selectedTab = badTab;
await appMenu.open();
appMenu.isReportBrokenSiteDisabled();
await appMenu.close();
gBrowser.selectedTab = goodTab1;
rbs = await protPanel.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
rbs.close();
closeTab(badTab);
closeTab(goodTab1);
closeTab(goodTab2);
});
add_task(async function testResetsProperlyOnWindowSwitch() {
ensureReportBrokenSitePreffedOn();
const tab1 = await openTab(REPORTABLE_PAGE_URL);
const win2 = await BrowserTestUtils.openNewBrowserWindow();
const tab2 = await openTab(REPORTABLE_PAGE_URL2, win2);
const appMenu1 = AppMenu();
const appMenu2 = ProtectionsPanel(win2);
let rbs2 = await appMenu2.openReportBrokenSite();
rbs2.isMainViewResetToCurrentTab();
rbs2.close();
// flip back to tab1's window and ensure its URL pops up instead of tab2's URL
await switchToWindow(window);
isSelectedTab(window, tab1); // sanity check
let rbs1 = await appMenu1.openReportBrokenSite();
rbs1.isMainViewResetToCurrentTab();
rbs1.close();
// likewise flip back to tab2's window and ensure its URL pops up instead of tab1's URL
await switchToWindow(win2);
isSelectedTab(win2, tab2); // sanity check
rbs2 = await appMenu2.openReportBrokenSite();
rbs2.isMainViewResetToCurrentTab();
rbs2.close();
closeTab(tab1);
closeTab(tab2);
await BrowserTestUtils.closeWindow(win2);
});

Просмотреть файл

@ -0,0 +1,679 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { CustomizableUITestUtils } = ChromeUtils.importESModule(
"resource://testing-common/CustomizableUITestUtils.sys.mjs"
);
const BASE_URL =
"https://example.com/browser/browser/components/reportbrokensite/test/browser/";
const REPORTABLE_PAGE_URL = "https://example.com";
const REPORTABLE_PAGE_URL2 = REPORTABLE_PAGE_URL.replace(".com", ".org");
const NEW_REPORT_ENDPOINT_TEST_URL = `${BASE_URL}/sendMoreInfoTestEndpoint.html`;
const PREFS = {
DATAREPORTING_ENABLED: "datareporting.healthreport.uploadEnabled",
REPORTER_ENABLED: "ui.new-webcompat-reporter.enabled",
REASON: "ui.new-webcompat-reporter.reason-dropdown",
SEND_MORE_INFO: "ui.new-webcompat-reporter.send-more-info-link",
NEW_REPORT_ENDPOINT: "ui.new-webcompat-reporter.new-report-endpoint",
REPORT_SITE_ISSUE_ENABLED: "extensions.webcompat-reporter.enabled",
};
function add_common_setup() {
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [[PREFS.NEW_REPORT_ENDPOINT, NEW_REPORT_ENDPOINT_TEST_URL]],
});
registerCleanupFunction(function () {
for (const prefName of Object.values(PREFS)) {
Services.prefs.clearUserPref(prefName);
}
});
});
}
async function openTab(url, win) {
const options = {
gBrowser:
win?.gBrowser ||
Services.wm.getMostRecentWindow("navigator:browser").gBrowser,
url,
};
return BrowserTestUtils.openNewForegroundTab(options);
}
async function changeTab(tab, url) {
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, url);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
}
function closeTab(tab) {
BrowserTestUtils.removeTab(tab);
}
function switchToWindow(win) {
const promises = [
BrowserTestUtils.waitForEvent(win, "focus"),
BrowserTestUtils.waitForEvent(win, "activate"),
];
win.focus();
return Promise.all(promises);
}
function isSelectedTab(win, tab) {
const selectedTab = win.document.querySelector(".tabbrowser-tab[selected]");
is(selectedTab, tab);
}
function ensureReportBrokenSitePreffedOn() {
Services.prefs.setBoolPref(PREFS.DATAREPORTING_ENABLED, true);
Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, true);
}
function ensureReportBrokenSitePreffedOff() {
Services.prefs.setBoolPref(PREFS.REPORTER_ENABLED, false);
}
function ensureReportSiteIssuePreffedOn() {
Services.prefs.setBoolPref(PREFS.REPORT_SITE_ISSUE_ENABLED, true);
}
function ensureReportSiteIssuePreffedOff() {
Services.prefs.setBoolPref(PREFS.REPORT_SITE_ISSUE_ENABLED, false);
}
function ensureSendMoreInfoEnabled() {
Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, true);
}
function ensureSendMoreInfoDisabled() {
Services.prefs.setBoolPref(PREFS.SEND_MORE_INFO, false);
}
function ensureReasonDisabled() {
Services.prefs.setIntPref(PREFS.REASON, 0);
}
function ensureReasonOptional() {
Services.prefs.setIntPref(PREFS.REASON, 1);
}
function ensureReasonRequired() {
Services.prefs.setIntPref(PREFS.REASON, 2);
}
function isMenuItemEnabled(menuItem, itemDesc) {
ok(!menuItem.hidden, `${itemDesc} menu item is shown`);
ok(!menuItem.disabled, `${itemDesc} menu item is enabled`);
}
function isMenuItemHidden(menuItem, itemDesc) {
ok(
!menuItem || menuItem.hidden || !BrowserTestUtils.is_visible(menuItem),
`${itemDesc} menu item is hidden`
);
}
function isMenuItemDisabled(menuItem, itemDesc) {
ok(!menuItem.hidden, `${itemDesc} menu item is shown`);
ok(menuItem.disabled, `${itemDesc} menu item is disabled`);
}
class ReportBrokenSiteHelper {
sourceMenu = undefined;
win = undefined;
constructor(sourceMenu) {
this.sourceMenu = sourceMenu;
this.win = sourceMenu.win;
}
get mainView() {
return this.win.document.getElementById(
"report-broken-site-popup-mainView"
);
}
get sentView() {
return this.win.document.getElementById(
"report-broken-site-popup-reportSentView"
);
}
get openPanel() {
return this.mainView?.closest("panel");
}
get opened() {
return this.openPanel?.hasAttribute("panelopen");
}
async open(triggerMenuItem) {
const window = triggerMenuItem.ownerGlobal;
const shownPromise = BrowserTestUtils.waitForEvent(
window,
"ViewShown",
true,
e => e.target.classList.contains("report-broken-site-view")
);
await EventUtils.synthesizeMouseAtCenter(triggerMenuItem, {}, window);
await shownPromise;
}
async #assertClickAndViewChanges(button, view, newView) {
ok(view.closest("panel").hasAttribute("panelopen"), "Panel is open");
ok(BrowserTestUtils.is_visible(button), "Button is visible");
ok(!button.disabled, "Button is enabled");
const promises = [];
if (newView) {
promises.push(BrowserTestUtils.waitForEvent(newView, "ViewShown"));
} else {
promises.push(BrowserTestUtils.waitForEvent(view, "ViewHiding"));
}
EventUtils.synthesizeMouseAtCenter(button, {}, this.win);
await Promise.all(promises);
}
async clickSend() {
await this.#assertClickAndViewChanges(
this.sendButton,
this.mainView,
this.sentView
);
}
async clickSendMoreInfo() {
const newTabPromise = BrowserTestUtils.waitForNewTab(
this.win.gBrowser,
NEW_REPORT_ENDPOINT_TEST_URL
);
EventUtils.synthesizeMouseAtCenter(this.sendMoreInfoLink, {}, this.win);
const newTab = await newTabPromise;
const receivedData = await SpecialPowers.spawn(
newTab.linkedBrowser,
[],
async function () {
await content.wrappedJSObject.messageArrived;
return content.wrappedJSObject.message;
}
);
this.win.gBrowser.removeCurrentTab();
return receivedData;
}
async clickCancel() {
await this.#assertClickAndViewChanges(this.cancelButton, this.mainView);
}
async clickOkay() {
await this.#assertClickAndViewChanges(this.okayButton, this.sentView);
}
close() {
if (this.opened) {
this.openPanel?.hidePopup(false);
}
this.sourceMenu?.close();
}
// UI element getters
get URLInput() {
return this.win.document.getElementById("report-broken-site-popup-url");
}
get reasonInput() {
return this.win.document.getElementById("report-broken-site-popup-reason");
}
get reasonLabelRequired() {
return this.win.document.getElementById(
"report-broken-site-popup-reason-label"
);
}
get reasonLabelOptional() {
return this.win.document.getElementById(
"report-broken-site-popup-reason-optional-label"
);
}
get descriptionTextarea() {
return this.win.document.getElementById(
"report-broken-site-popup-description"
);
}
get sendMoreInfoLink() {
return this.win.document.getElementById(
"report-broken-site-popup-send-more-info-link"
);
}
get sendButton() {
return this.win.document.getElementById(
"report-broken-site-popup-send-button"
);
}
get cancelButton() {
return this.win.document.getElementById(
"report-broken-site-popup-cancel-button"
);
}
get okayButton() {
return this.win.document.getElementById(
"report-broken-site-popup-okay-button"
);
}
// Test helpers
#setInput(input, value) {
input.value = value;
input.dispatchEvent(
new UIEvent("input", { bubbles: true, view: this.win })
);
}
setURL(value) {
this.#setInput(this.URLInput, value);
}
chooseReason(value) {
const item = this.win.document.getElementById(
`report-broken-site-popup-reason-${value}`
);
const input = this.reasonInput;
input.selectedItem = item;
input.dispatchEvent(
new UIEvent("command", { bubbles: true, view: this.win })
);
}
setDescription(value) {
this.#setInput(this.descriptionTextarea, value);
}
isURL(expected) {
is(this.URLInput.value, expected);
}
isSendButtonEnabled() {
ok(BrowserTestUtils.is_visible(this.sendButton), "Send button is visible");
ok(!this.sendButton.disabled, "Send button is enabled");
}
isSendButtonDisabled() {
ok(BrowserTestUtils.is_visible(this.sendButton), "Send button is visible");
ok(this.sendButton.disabled, "Send button is disabled");
}
isSendMoreInfoShown() {
ok(
BrowserTestUtils.is_visible(this.sendMoreInfoLink),
"send more info is shown"
);
}
isSendMoreInfoHidden() {
ok(
!BrowserTestUtils.is_visible(this.sendMoreInfoLink),
"send more info is hidden"
);
}
isSendMoreInfoShownOrHiddenAppropriately() {
if (Services.prefs.getBoolPref(PREFS.SEND_MORE_INFO)) {
this.isSendMoreInfoShown();
} else {
this.isSendMoreInfoHidden();
}
}
isReasonHidden() {
ok(
!BrowserTestUtils.is_visible(this.reasonInput),
"reason drop-down is hidden"
);
ok(
!BrowserTestUtils.is_visible(this.reasonLabelOptional),
"optional reason label is hidden"
);
ok(
!BrowserTestUtils.is_visible(this.reasonLabelRequired),
"required reason label is hidden"
);
}
isReasonRequired() {
ok(
BrowserTestUtils.is_visible(this.reasonInput),
"reason drop-down is shown"
);
ok(
!BrowserTestUtils.is_visible(this.reasonLabelOptional),
"optional reason label is hidden"
);
ok(
BrowserTestUtils.is_visible(this.reasonLabelRequired),
"required reason label is shown"
);
}
isReasonOptional() {
ok(
BrowserTestUtils.is_visible(this.reasonInput),
"reason drop-down is shown"
);
ok(
BrowserTestUtils.is_visible(this.reasonLabelOptional),
"optional reason label is shown"
);
ok(
!BrowserTestUtils.is_visible(this.reasonLabelRequired),
"required reason label is hidden"
);
}
isReasonShownOrHiddenAppropriately() {
const pref = Services.prefs.getIntPref(PREFS.REASON);
if (pref == 2) {
this.isReasonOptional();
} else if (pref == 1) {
this.isReasonOptional();
} else {
this.isReasonHidden();
}
}
isDescription(expected) {
return this.descriptionTextarea.value == expected;
}
isMainViewResetToCurrentTab() {
this.isURL(this.win.gBrowser.selectedBrowser.currentURI.spec);
this.isDescription("");
this.isReasonShownOrHiddenAppropriately();
this.isSendMoreInfoShownOrHiddenAppropriately();
}
}
class MenuHelper {
menuDescription = undefined;
win = undefined;
constructor(win = window) {
this.win = win;
}
get reportBrokenSite() {}
get reportSiteIssue() {}
get popup() {}
get opened() {
return this.popup?.hasAttribute("panelopen");
}
async open() {}
async close() {}
isReportBrokenSiteDisabled() {
return isMenuItemDisabled(this.reportBrokenSite, this.menuDescription);
}
isReportBrokenSiteEnabled() {
return isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
}
isReportBrokenSiteHidden() {
return isMenuItemHidden(this.reportBrokenSite, this.menuDescription);
}
isReportSiteIssueDisabled() {
return isMenuItemDisabled(this.reportSiteIssue, this.menuDescription);
}
isReportSiteIssueEnabled() {
return isMenuItemEnabled(this.reportSiteIssue, this.menuDescription);
}
isReportSiteIssueHidden() {
return isMenuItemHidden(this.reportSiteIssue, this.menuDescription);
}
async openReportBrokenSite() {
if (!this.opened) {
await this.open();
}
isMenuItemEnabled(this.reportBrokenSite, this.menuDescription);
const rbs = new ReportBrokenSiteHelper(this);
await rbs.open(this.reportBrokenSite);
return rbs;
}
async openAndPrefillReportBrokenSite(url = null, description = "") {
let rbs = await this.openReportBrokenSite();
rbs.isMainViewResetToCurrentTab();
if (url) {
rbs.setURL(url);
}
if (description) {
rbs.setDescription(description);
}
return rbs;
}
}
class AppMenuHelper extends MenuHelper {
menuDescription = "AppMenu";
get reportBrokenSite() {
return this.win.PanelMultiView.getViewNode(
this.win.document,
"appMenu-report-broken-site-button"
);
}
get reportSiteIssue() {
return undefined;
}
get popup() {
return this.win.PanelMultiView.getViewNode(
this.win.document,
"appMenu-popup"
);
}
async open() {
await new CustomizableUITestUtils(this.win).openMainMenu();
}
async close() {
if (this.opened) {
await new CustomizableUITestUtils(this.win).hideMainMenu();
}
}
}
class AppMenuHelpSubmenuHelper extends MenuHelper {
menuDescription = "AppMenu help sub-menu";
get reportBrokenSite() {
return this.win.PanelMultiView.getViewNode(
this.win.document,
"appMenu_help_reportBrokenSite"
);
}
get reportSiteIssue() {
return this.win.PanelMultiView.getViewNode(
this.win.document,
"appMenu_help_reportSiteIssue"
);
}
get popup() {
return this.win.PanelMultiView.getViewNode(
this.win.document,
"appMenu-popup"
);
}
async open() {
await new CustomizableUITestUtils(this.win).openMainMenu();
const anchor = this.win.document.getElementById("PanelUI-menu-button");
this.win.PanelUI.showHelpView(anchor);
const appMenuHelpSubview =
this.win.document.getElementById("PanelUI-helpView");
await BrowserTestUtils.waitForEvent(appMenuHelpSubview, "ViewShowing");
}
async close() {
if (this.opened) {
await new CustomizableUITestUtils(this.win).hideMainMenu();
}
}
}
class HelpMenuHelper extends MenuHelper {
menuDescription = "Help Menu";
get reportBrokenSite() {
return this.win.PanelMultiView.getViewNode(
this.win.document,
"help_reportBrokenSite"
);
}
get reportSiteIssue() {
return this.win.PanelMultiView.getViewNode(
this.win.document,
"help_reportSiteIssue"
);
}
get popup() {
return this.win.PanelMultiView.getViewNode(
this.win.document,
"menu_HelpPopup"
);
}
async open() {
const popup = this.popup;
const promise = BrowserTestUtils.waitForEvent(popup, "popupshown");
// This event-faking method was copied from browser_title_case_menus.js so
// this can be tested on MacOS (where the actual menus cannot be opened in
// tests, but we only need the help menu to populate itself).
popup.dispatchEvent(new MouseEvent("popupshowing", { bubbles: true }));
popup.dispatchEvent(new MouseEvent("popupshown", { bubbles: true }));
await promise;
}
async close() {
const popup = this.popup;
const promise = BrowserTestUtils.waitForEvent(popup, "popuphidden");
// (Also copied from browser_title_case_menus.js)
// Just for good measure, we'll fire the popuphiding/popuphidden events
// after we close the menupopups.
popup.dispatchEvent(new MouseEvent("popuphiding", { bubbles: true }));
popup.dispatchEvent(new MouseEvent("popuphidden", { bubbles: true }));
await promise;
}
async openReportBrokenSite() {
// since we can't open the help menu on all OSes, we just directly open RBS instead.
await this.open();
const shownPromise = BrowserTestUtils.waitForEvent(
this.win,
"ViewShown",
true,
e => e.target.classList.contains("report-broken-site-view")
);
ReportBrokenSite.open({
sourceEvent: {
target: this.reportBrokenSite,
},
view: this.win,
});
await shownPromise;
return new ReportBrokenSiteHelper(this);
}
}
class ProtectionsPanelHelper extends MenuHelper {
menuDescription = "Protections Panel";
get reportBrokenSite() {
this.win.gProtectionsHandler._initializePopup();
return this.win.PanelMultiView.getViewNode(
this.win.document,
"protections-popup-report-broken-site-button"
);
}
get reportSiteIssue() {
return undefined;
}
get popup() {
this.win.gProtectionsHandler._initializePopup();
return this.win.PanelMultiView.getViewNode(
this.win.document,
"protections-popup"
);
}
async open() {
const promise = BrowserTestUtils.waitForEvent(
this.win,
"popupshown",
true,
e => e.target.id == "protections-popup"
);
this.win.gProtectionsHandler.showProtectionsPopup();
await promise;
}
async close() {
if (this.opened) {
const popup = this.popup;
const promise = BrowserTestUtils.waitForEvent(popup, "popuphidden");
this.win.PanelMultiView.hidePopup(popup, false);
await promise;
}
}
}
function AppMenu(win = window) {
return new AppMenuHelper(win);
}
function AppMenuHelpSubmenu(win = window) {
return new AppMenuHelpSubmenuHelper(win);
}
function HelpMenu(win = window) {
return new HelpMenuHelper(win);
}
function ProtectionsPanel(win = window) {
return new ProtectionsPanelHelper(win);
}

Просмотреть файл

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<!-- 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/. -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
</head>
<body>
<script>
let ready;
window.wrtReady = new Promise(r => ready = r);
let arrived;
window.messageArrived = new Promise(r => arrived = r);
window.addEventListener("message", e => {
window.message = e.data;
arrived();
});
window.addEventListener("load", () => {
setTimeout(ready, 100);
});
</script>
</body>
</html>

Просмотреть файл

@ -23,6 +23,7 @@
rgba(207, 207, 216, .40)
);
--button-color: light-dark(rgb(21, 20, 26), rgb(251, 251, 254));
--button-color-inverted: light-dark(rgb(251, 251, 254), rgb(21, 20, 26));
--focus-outline-color: var(--button-primary-bgcolor);

Просмотреть файл

@ -2012,3 +2012,147 @@ panelview:not([mainview]) #PanelUI-whatsNew-title {
#reset-pbm-panel-checkbox {
margin-inline: 0 8px;
}
/* Report Broken Site panels */
.report-broken-site-view .panel-subview-body * {
font: menu;
}
.report-broken-site-view button {
font-weight: var(--font-weight-bold);
}
.report-broken-site-view p {
line-height: 1.5em;
}
.report-broken-site-view.main-view p {
margin-block-end: 0;
}
.report-broken-site-view .deemphasized,
.report-broken-site-view .missing {
color: inherit;
font-size: 96%;
}
#report-broken-site-popup-reason,
#report-broken-site-popup-description {
margin-block-start: 0;
}
.report-broken-site-view p.missing {
font-style: italic;
}
.report-broken-site-view p.missing {
margin-top: 0;
}
.report-broken-site-view menulist {
padding-inline-start: 0.25em;
}
.report-broken-site-view input,
.report-broken-site-view textarea {
border: 1px solid var(--panel-separator-color);
background-color: var(--button-color-inverted);
margin: 0;
padding: 0.25em;
}
.report-broken-site-view menulist {
padding: 0.5em 1em;
}
.report-broken-site-view input:disabled {
color: inherit;
}
.report-broken-site-view.main-view #report-broken-site-panel-container > label {
margin-block: 1.5em 0.5em;
margin-inline: 0;
}
#report-broken-site-panel-container > hbox,
#report-broken-site-panel-container > a {
margin-block: 1.5em;
}
.report-broken-site-view p + a {
margin-block: 0;
}
.report-broken-site-view {
max-width: 26em;
}
#report-broken-site-popup-url-label {
margin-block-start: 1em;
}
.report-broken-site-view button[disabled="true"] {
opacity: 0.5;
}
#report-broken-site-panel-container {
margin: var(--arrowpanel-menuitem-margin);
padding: var(--arrowpanel-menuitem-padding);
}
#report-broken-site-panel-container > p,
#report-broken-site-panel-container > label,
#report-broken-site-panel-container > input,
#report-broken-site-panel-container > textarea,
#report-broken-site-panel-container > option {
color: var(--panel-color);
background: var(--panel-background);
}
.report-broken-site-view.sent-view .panel-header {
-moz-context-properties: fill;
fill: #2ac3a2;
background-image: url("chrome://global/skin/icons/check-filled.svg");
background-repeat: no-repeat;
background-position: calc(var(--panel-separator-margin-horizontal) +
var(--arrowpanel-menuitem-padding-inline)) center;
}
.report-broken-site-view.sent-view .subviewbutton-back {
visibility: hidden;
}
.report-broken-site-view.sent-view {
background-color: var(--color-background-success);
}
.report-broken-site-view moz-button-group.panel-footer {
margin-inline-end: 0;
}
.report-broken-site-view moz-button-group > button {
background-color: var(--button-bgcolor);
color: var(--button-color);
}
.report-broken-site-view moz-button-group > button:hover {
background-color: var(--button-hover-bgcolor);
}
.report-broken-site-view moz-button-group > button:active {
background-color: var(--button-active-bgcolor);
}
.report-broken-site-view moz-button-group > button.primary {
background-color: var(--button-primary-bgcolor);
color: var(--button-primary-color);
}
.report-broken-site-view moz-button-group > button.primary:hover {
background-color: var(--button-primary-hover-bgcolor);
}
.report-broken-site-view moz-button-group > button.primary:active {
background-color: var(--button-primary-active-bgcolor);
}

Просмотреть файл

@ -62,6 +62,7 @@ DIRS += [
"remotebrowserutils",
"reflect",
"reputationservice",
"reportbrokensite",
"resistfingerprinting",
"search",
"sessionstore",

Просмотреть файл

@ -1929,6 +1929,30 @@ backgroundThreads:
type: boolean
setPref: threads.lower_mainthread_priority_in_background.enabled
reportBrokenSite:
description: the Report Broken Site feature
hasExposure: false
isEarlyStartup: true
variables:
enabled:
type: boolean
setPref: ui.new-webcompat-reporter.enabled
description: >-
Whether Report Broken Site is enabled
sendMoreInfo:
type: boolean
setPref: ui.new-webcompat-reporter.send-more-info-link
description: >-
Whether Report Broken Site shows the send more info link directing
users to webcompat.com (defaults to true for prerelease channels)
reasonDropdown:
type: int
setPref: ui.new-webcompat-reporter.reason-dropdown
description: >-
0 = do not show the "reason" dropdown
1 = show an optional "reason" dropdown
2 = show a required "reason" dropdown
feltPrivacy:
description: Prefs for Felt Privacy v1 experiments
owner: cmeador@mozilla.com

Просмотреть файл

@ -0,0 +1,558 @@
/* 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/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const SCREENSHOT_FORMAT = { format: "jpeg", quality: 75 };
function RunScriptInFrame(win, script) {
const contentPrincipal = win.document.nodePrincipal;
const sandbox = Cu.Sandbox([contentPrincipal], {
sandboxName: "Report Broken Site webcompat.com helper",
sandboxPrototype: win,
sameZoneAs: win,
originAttributes: contentPrincipal.originAttributes,
});
return Cu.evalInSandbox(script, sandbox, null, "sandbox eval code", 1);
}
class ConsoleLogHelper {
static PREVIEW_MAX_ITEMS = 10;
static LOG_LEVELS = ["debug", "info", "warn", "error"];
#windowId = undefined;
constructor(windowId) {
this.#windowId = windowId;
}
getLoggedMessages(alsoIncludePrivate = true) {
return this.getConsoleAPIMessages().concat(
this.getScriptErrors(alsoIncludePrivate)
);
}
getConsoleAPIMessages() {
const ConsoleAPIStorage = Cc[
"@mozilla.org/consoleAPI-storage;1"
].getService(Ci.nsIConsoleAPIStorage);
let messages = ConsoleAPIStorage.getEvents(this.#windowId);
return messages.map(evt => {
const { columnNumber, filename, level, lineNumber, timeStamp } = evt;
const args = [];
for (const arg of evt.arguments) {
args.push(this.#getArgs(arg));
}
const message = {
level,
log: args,
uri: filename,
pos: `${lineNumber}:${columnNumber}`,
};
return { timeStamp, message };
});
}
getScriptErrors(alsoIncludePrivate) {
const messages = Services.console.getMessageArray();
return messages
.filter(message => {
if (message instanceof Ci.nsIScriptError) {
if (!alsoIncludePrivate && message.isFromPrivateWindow) {
return false;
}
if (this.#windowId && this.#windowId !== message.innerWindowID) {
return false;
}
return true;
}
// If this is not an nsIScriptError and we need to do window-based
// filtering we skip this message.
return false;
})
.map(error => {
const {
timeStamp,
errorMessage,
sourceName,
lineNumber,
columnNumber,
logLevel,
} = error;
const message = {
level: ConsoleLogHelper.LOG_LEVELS[logLevel],
log: [errorMessage],
uri: sourceName,
pos: `${lineNumber}:${columnNumber}`,
};
return { timeStamp, message };
});
}
#getPreview(value) {
switch (typeof value) {
case "symbol":
return value.toString();
case "function":
return "function ()";
case "object":
if (value === null) {
return null;
}
if (Array.isArray(value)) {
return `(${value.length})[...]`;
}
return "{...}";
case "undefined":
return "undefined";
default:
try {
structuredClone(value);
} catch (_) {
return `${value}` || "?";
}
return value;
}
}
#getArrayPreview(arr) {
const preview = [];
let count = 0;
for (const value of arr) {
if (++count > ConsoleLogHelper.PREVIEW_MAX_ITEMS) {
break;
}
preview.push(this.#getPreview(value));
}
return preview;
}
#getObjectPreview(obj) {
const preview = {};
let count = 0;
for (const key of Object.keys(obj)) {
if (++count > ConsoleLogHelper.PREVIEW_MAX_ITEMS) {
break;
}
preview[key] = this.#getPreview(obj[key]);
}
return preview;
}
#getArgs(value) {
if (typeof value === "object" && value !== null) {
if (Array.isArray(value)) {
return this.#getArrayPreview(value);
}
return this.#getObjectPreview(value);
}
return this.#getPreview(value);
}
}
const FrameworkDetector = {
hasFastClickPageScript(window) {
if (window.FastClick) {
return true;
}
for (const property in window) {
try {
const proto = window[property].prototype;
if (proto && proto.needsClick) {
return true;
}
} catch (_) {}
}
return false;
},
hasMobifyPageScript(window) {
return !!window.Mobify?.Tag;
},
hasMarfeelPageScript(window) {
return !!window.marfeel;
},
checkWindow(window) {
const script = `
(function() {
function ${FrameworkDetector.hasFastClickPageScript};
function ${FrameworkDetector.hasMobifyPageScript};
function ${FrameworkDetector.hasMarfeelPageScript};
const win = window.wrappedJSObject || window;
return {
fastclick: hasFastClickPageScript(win),
mobify: hasMobifyPageScript(win),
marfeel: hasMarfeelPageScript(win),
}
})();
`;
return RunScriptInFrame(window, script);
},
};
function getSysinfoProperty(propertyName, defaultValue) {
try {
return Services.sysinfo.getProperty(propertyName);
} catch (e) {}
return defaultValue;
}
function limitStringToLength(str, maxLength) {
if (typeof str !== "string") {
return null;
}
return str.substring(0, maxLength);
}
const BrowserInfo = {
getAppInfo() {
const { userAgent } = Cc[
"@mozilla.org/network/protocol;1?name=http"
].getService(Ci.nsIHttpProtocolHandler);
return {
applicationName: Services.appinfo.name,
buildId: Services.appinfo.appBuildID,
defaultUserAgent: userAgent,
updateChannel: AppConstants.MOZ_UPDATE_CHANNEL,
version: Services.appinfo.version,
};
},
getPrefs() {
const prefs = {};
for (const [name, dflt] of Object.entries({
"layers.acceleration.force-enabled": undefined,
"gfx.webrender.software": undefined,
"browser.opaqueResponseBlocking": undefined,
"extensions.InstallTrigger.enabled": undefined,
"privacy.resistFingerprinting": undefined,
"privacy.globalprivacycontrol.enabled": undefined,
})) {
prefs[name] = Services.prefs.getBoolPref(name, dflt);
}
const cookieBehavior = "network.cookie.cookieBehavior";
prefs[cookieBehavior] = Services.prefs.getIntPref(cookieBehavior);
return prefs;
},
getPlatformInfo() {
let memoryMB = getSysinfoProperty("memsize", null);
if (memoryMB) {
memoryMB = Math.round(memoryMB / 1024 / 1024);
}
const info = {
fissionEnabled: Services.appinfo.fissionAutostart,
memoryMB,
osArchitecture: getSysinfoProperty("arch", null),
osName: getSysinfoProperty("name", null),
osVersion: getSysinfoProperty("version", null),
os: AppConstants.platform,
};
if (AppConstants.platform === "android") {
info.device = getSysinfoProperty("device", null);
info.isTablet = getSysinfoProperty("tablet", false);
}
return info;
},
getSecurityInfo() {
if (AppConstants.platform != "win") {
return undefined;
}
const maxStringLength = 256;
const keys = [
["registeredAntiVirus", "antivirus"],
["registeredAntiSpyware", "antispyware"],
["registeredFirewall", "firewall"],
];
let result = {};
for (let [inKey, outKey] of keys) {
let prop = getSysinfoProperty(inKey, null);
if (prop) {
prop = limitStringToLength(prop, maxStringLength).split(";");
}
result[outKey] = prop;
}
return result;
},
getAllData() {
return {
app: BrowserInfo.getAppInfo(),
prefs: BrowserInfo.getPrefs(),
platform: BrowserInfo.getPlatformInfo(),
security: BrowserInfo.getSecurityInfo(),
};
},
};
export class ReportBrokenSiteChild extends JSWindowActorChild {
#getWebCompatInfo(docShell) {
return Promise.all([
this.#getConsoleLogs(docShell),
this.sendQuery(
"GetWebcompatInfoOnlyAvailableInParentProcess",
SCREENSHOT_FORMAT
),
]).then(([consoleLog, infoFromParent]) => {
const { antitracking, graphics, locales, screenshot } = infoFromParent;
const browser = BrowserInfo.getAllData();
browser.graphics = graphics;
browser.locales = locales;
const win = docShell.domWindow;
const frameworks = FrameworkDetector.checkWindow(win);
if (browser.platform.os !== "linux") {
delete browser.prefs["layers.acceleration.force-enabled"];
}
return {
antitracking,
browser,
consoleLog,
devicePixelRatio: win.devicePixelRatio,
frameworks,
languages: win.navigator.languages,
screenshot,
url: win.location.href,
userAgent: win.navigator.userAgent,
};
});
}
async #getConsoleLogs(docShell) {
return this.#getLoggedMessages()
.flat()
.sort((a, b) => a.timeStamp - b.timeStamp)
.map(m => m.message);
}
#getLoggedMessages(alsoIncludePrivate = false) {
const windowId = this.contentWindow.windowGlobalChild.innerWindowId;
const helper = new ConsoleLogHelper(windowId, alsoIncludePrivate);
return helper.getLoggedMessages();
}
#formatReportDataForWebcompatCom({
reason,
description,
reportUrl,
reporterConfig,
webcompatInfo,
}) {
const extra_labels = [];
const message = Object.assign({}, reporterConfig, {
url: reportUrl,
category: reason,
description,
details: {},
extra_labels,
});
const payload = {
message,
};
if (webcompatInfo) {
const {
antitracking,
browser,
devicePixelRatio,
consoleLog,
frameworks,
languages,
screenshot,
url,
userAgent,
} = webcompatInfo;
const {
blockList,
isPrivateBrowsing,
hasMixedActiveContentBlocked,
hasMixedDisplayContentBlocked,
hasTrackingContentBlocked,
} = antitracking;
message.blockList = blockList;
const { app, graphics, prefs, platform, security } = browser;
const { applicationName, version, updateChannel, defaultUserAgent } = app;
const {
fissionEnabled,
memoryMb,
osArchitecture,
osName,
osVersion,
device,
isTablet,
} = platform;
const additionalData = {
applicationName,
blockList,
devicePixelRatio,
finalUserAgent: userAgent,
fissionEnabled,
gfxData: graphics,
hasMixedActiveContentBlocked,
hasMixedDisplayContentBlocked,
hasTrackingContentBlocked,
isPB: isPrivateBrowsing,
languages,
memoryMb,
osArchitecture,
osName,
osVersion,
prefs,
updateChannel,
userAgent: defaultUserAgent,
version,
};
if (security !== undefined) {
additionalData.sec = security;
}
if (device !== undefined) {
additionalData.device = device;
}
if (isTablet !== undefined) {
additionalData.isTablet = isTablet;
}
const specialPrefs = {};
for (const pref of [
"layers.acceleration.force-enabled",
"gfx.webrender.software",
]) {
specialPrefs[pref] = prefs[pref];
}
const details = Object.assign(message.details, specialPrefs, {
additionalData,
buildId: browser.buildId,
blockList,
channel: browser.updateChannel,
hasTouchScreen: browser.graphics.hasTouchScreen,
});
// If the user enters a URL unrelated to the current tab,
// don't bother sending a screnshot or logs/etc
let sendRecordedPageSpecificDetails = false;
try {
const givenUri = new URL(reportUrl);
const recordedUri = new URL(url);
sendRecordedPageSpecificDetails =
givenUri.origin == recordedUri.origin &&
givenUri.pathname == recordedUri.pathname;
} catch (_) {}
if (sendRecordedPageSpecificDetails) {
payload.screenshot = screenshot;
details.consoleLog = consoleLog;
details.frameworks = frameworks;
details["mixed active content blocked"] =
antitracking.hasMixedActiveContentBlocked;
details["mixed passive content blocked"] =
antitracking.hasMixedDisplayContentBlocked;
details["tracking content blocked"] =
antitracking.hasTrackingContentBlocked
? `true (${antitracking.blockList})`
: "false";
if (antitracking.hasTrackingContentBlocked) {
extra_labels.push(
`type-tracking-protection-${antitracking.blockList}`
);
}
for (const [framework, active] of Object.entries(frameworks)) {
if (!active) {
continue;
}
details[framework] = true;
extra_labels.push(`type-${framework}`);
}
}
}
return payload;
}
#stripNonASCIIChars(str) {
// eslint-disable-next-line no-control-regex
return str.replace(/[^\x00-\x7F]/g, "");
}
async receiveMessage(msg) {
const { docShell } = this;
switch (msg.name) {
case "SendDataToWebcompatCom": {
const win = docShell.domWindow;
const expectedEndpoint = msg.data.endpointUrl;
if (win.location.href == expectedEndpoint) {
// Ensure that the tab has fully loaded and is waiting for messages
const onLoad = () => {
const payload = this.#formatReportDataForWebcompatCom(msg.data);
const json = this.#stripNonASCIIChars(JSON.stringify(payload));
const expectedOrigin = JSON.stringify(
new URL(expectedEndpoint).origin
);
// webcompat.com checks that the message comes from its own origin
const script = `
const wrtReady = window.wrappedJSObject?.wrtReady;
if (wrtReady) {
console.info("Report Broken Site is waiting");
}
Promise.resolve(wrtReady).then(() => {
console.debug(${json});
postMessage(${json}, ${expectedOrigin})
});`;
RunScriptInFrame(win, script);
};
if (win.document.readyState == "complete") {
onLoad();
} else {
win.addEventListener("load", onLoad, { once: true });
}
}
return null;
}
case "GetWebCompatInfo": {
return this.#getWebCompatInfo(docShell);
}
case "GetConsoleLog": {
return this.#getLoggedMessages();
}
}
return null;
}
}

Просмотреть файл

@ -0,0 +1,263 @@
/* vim: set ts=2 sw=2 et tw=80: */
/* 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/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
class DriverInfo {
constructor(gl, ext) {
try {
this.extensions = ext.getParameter(ext.EXTENSIONS);
} catch (e) {}
try {
this.renderer = ext.getParameter(gl.RENDERER);
} catch (e) {}
try {
this.vendor = ext.getParameter(gl.VENDOR);
} catch (e) {}
try {
this.version = ext.getParameter(gl.VERSION);
} catch (e) {}
try {
this.wsiInfo = ext.getParameter(ext.WSI_INFO);
} catch (e) {}
}
equals(info2) {
return this.renderer == info2.renderer && this.version == info2.version;
}
static getByType(driver) {
const doc = new DOMParser().parseFromString("<html/>", "text/html");
const canvas = doc.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
let error;
canvas.addEventListener("webglcontextcreationerror", function (e) {
error = true;
});
let gl = null;
try {
gl = canvas.getContext(driver);
} catch (e) {
error = true;
}
if (error || !gl?.getExtension) {
return undefined;
}
let ext = null;
try {
ext = gl.getExtension("MOZ_debug");
} catch (e) {}
if (!ext) {
return undefined;
}
const data = new DriverInfo(gl, ext);
try {
gl.getExtension("WEBGL_lose_context").loseContext();
} catch (e) {}
return data;
}
static getAll() {
const drivers = [];
function tryDriver(type) {
const driver = DriverInfo.getByType(type);
if (driver && !drivers.find(d => d.equals(driver))) {
drivers.push(driver);
}
}
tryDriver("webgl");
tryDriver("webgl2");
tryDriver("webgpu");
return drivers;
}
}
export class ReportBrokenSiteParent extends JSWindowActorParent {
#getAntitrackingBlockList() {
// If content-track-digest256 is in the tracking table,
// the user has enabled the strict list.
const trackingTable = Services.prefs.getCharPref(
"urlclassifier.trackingTable"
);
return trackingTable.includes("content") ? "strict" : "basic";
}
#getAntitrackingInfo(browsingContext) {
return {
blockList: this.#getAntitrackingBlockList(),
isPrivateBrowsing: browsingContext.usePrivateBrowsing,
hasTrackingContentBlocked: !!(
browsingContext.currentWindowGlobal.contentBlockingEvents &
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT
),
hasMixedActiveContentBlocked: !!(
browsingContext.secureBrowserUI.state &
Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT
),
hasMixedDisplayContentBlocked: !!(
browsingContext.secureBrowserUI.state &
Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT
),
};
}
#getBasicGraphicsInfo(gfxInfo) {
const get = name => {
try {
return gfxInfo[name];
} catch (e) {}
return undefined;
};
const clean = rawObj => {
const obj = JSON.parse(JSON.stringify(rawObj));
if (!Object.keys(obj).length) {
return undefined;
}
return obj;
};
const cleanDevice = (vendorID, deviceID, subsysID) => {
return clean({ vendorID, deviceID, subsysID });
};
const d1 = cleanDevice(
get("adapterVendorID"),
get("adapterDeviceID"),
get("adapterSubsysID")
);
const d2 = cleanDevice(
get("adapterVendorID2"),
get("adapterDeviceID2"),
get("adapterSubsysID2")
);
const devices = (get("isGPU2Active") ? [d2, d1] : [d1, d2]).filter(
v => v !== undefined
);
return clean({
direct2DEnabled: get("direct2DEnabled"),
directWriteEnabled: get("directWriteEnabled"),
directWriteVersion: get("directWriteVersion"),
hasTouchScreen: gfxInfo.getInfo().ApzTouchInput == 1,
cleartypeParameters: get("clearTypeParameters"),
targetFrameRate: get("targetFrameRate"),
devices,
});
}
#getGraphicsInfo() {
const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
const data = this.#getBasicGraphicsInfo(gfxInfo);
const drivers = DriverInfo.getAll();
if (drivers.length) {
data.drivers = drivers.map(({ renderer, version }) => {
return { renderer, version };
});
}
try {
const info = gfxInfo.CodecSupportInfo;
if (info) {
const codecs = {};
for (const item of gfxInfo.CodecSupportInfo.split("\n")) {
const [codec, type] = item.split(" ");
if (!codecs[codec]) {
codecs[codec] = { hw: false, sw: false };
}
if (type == "SW") {
codecs[codec].software = true;
} else if (type == "HW") {
codecs[codec].hardware = true;
}
}
data.codecSupport = codecs;
}
} catch (e) {}
try {
const { features } = gfxInfo.getFeatureLog();
data.features = {};
for (let { name, log, status } of features) {
for (const item of log.reverse()) {
if (!item.failureId || item.status != status) {
continue;
}
status = `${status} (${item.message || item.failureId})`;
}
data.features[name] = status;
}
} catch (e) {}
try {
if (AppConstants.platform !== "android") {
data.monitors = gfxInfo.getMonitors();
}
} catch (e) {}
return data;
}
async #getScreenshot(browsingContext, format, quality) {
const zoom = browsingContext.fullZoom;
const scale = browsingContext.topChromeWindow?.devicePixelRatio || 1;
const wgp = browsingContext.currentWindowGlobal;
const image = await wgp.drawSnapshot(
undefined, // rect
scale * zoom,
"white",
undefined // resetScrollPosition
);
const doc = Services.appShell.hiddenDOMWindow.document;
const canvas = doc.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d", { alpha: false });
ctx.drawImage(image, 0, 0);
image.close();
return canvas.toDataURL(`image/${format}`, quality / 100);
}
async receiveMessage(msg) {
switch (msg.name) {
case "GetWebcompatInfoOnlyAvailableInParentProcess": {
const { format, quality } = msg.data;
const screenshot = await this.#getScreenshot(
msg.target.browsingContext,
format,
quality
).catch(e => {
console.error("Report Broken Site: getting a screenshot failed", e);
return Promise.resolve(undefined);
});
return {
antitracking: this.#getAntitrackingInfo(msg.target.browsingContext),
graphics: this.#getGraphicsInfo(),
locales: Services.locale.availableLocales,
screenshot,
};
}
}
return null;
}
}

Просмотреть файл

@ -0,0 +1,13 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
FINAL_TARGET_FILES.actors = [
"ReportBrokenSiteChild.sys.mjs",
"ReportBrokenSiteParent.sys.mjs",
]
with Files("**"):
BUG_COMPONENT = ("Web Compatibility", "Desktop")

Просмотреть файл

@ -472,6 +472,23 @@ let JSWINDOWACTORS = {
allFrames: true,
},
ReportBrokenSite: {
parent: {
esModuleURI: "resource://gre/actors/ReportBrokenSiteParent.sys.mjs",
},
child: {
esModuleURI: "resource://gre/actors/ReportBrokenSiteChild.sys.mjs",
},
matches: [
"http://*/*",
"https://*/*",
"about:certerror?*",
"about:neterror?*",
],
messageManagerGroups: ["browsers"],
allFrames: true,
},
// This actor is available for all pages that one can
// view the source of, however it won't be created until a
// request to view the source is made via the message