Bug 1580554 - Open abuse report panel in a separate dialog window. r=mstriemer

Introduced a new "extensions.abuseReport.openDialog" pref:
- when set to false (current default): the abuse report panel is opened
  as a subframe of the about:addons tab
- when set to true: the abuse report panel is opened in its own dialog window

Differential Revision: https://phabricator.services.mozilla.com/D45570

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Luca Greco 2019-10-07 16:02:38 +00:00
Родитель 45b356558e
Коммит b62acf4aca
9 изменённых файлов: 425 добавлений и 89 удалений

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

@ -2303,6 +2303,8 @@ pref("services.settings.security.onecrl.signer", "onecrl.content-signature.mozil
pref("services.settings.security.onecrl.checked", 0); pref("services.settings.security.onecrl.checked", 0);
pref("extensions.abuseReport.enabled", true); pref("extensions.abuseReport.enabled", true);
// Opened as a sub-frame of the about:addons page when set to false.
pref("extensions.abuseReport.openDialog", false);
pref("extensions.abuseReport.url", "https://services.addons.mozilla.org/api/v4/abuse/report/addon/"); pref("extensions.abuseReport.url", "https://services.addons.mozilla.org/api/v4/abuse/report/addon/");
// Blocklist preferences // Blocklist preferences

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

@ -11,6 +11,7 @@ const { XPCOMUtils } = ChromeUtils.import(
Cu.importGlobalProperties(["fetch"]); Cu.importGlobalProperties(["fetch"]);
const PREF_ABUSE_REPORT_URL = "extensions.abuseReport.url"; const PREF_ABUSE_REPORT_URL = "extensions.abuseReport.url";
const PREF_ABUSE_REPORT_OPEN_DIALOG = "extensions.abuseReport.openDialog";
// Maximum length of the string properties sent to the API endpoint. // Maximum length of the string properties sent to the API endpoint.
const MAX_STRING_LENGTH = 255; const MAX_STRING_LENGTH = 255;
@ -32,6 +33,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
PREF_ABUSE_REPORT_URL PREF_ABUSE_REPORT_URL
); );
XPCOMUtils.defineLazyPreferenceGetter(
this,
"SHOULD_OPEN_DIALOG",
PREF_ABUSE_REPORT_OPEN_DIALOG,
false
);
const PRIVATE_REPORT_PROPS = Symbol("privateReportProps"); const PRIVATE_REPORT_PROPS = Symbol("privateReportProps");
const ERROR_TYPES = Object.freeze([ const ERROR_TYPES = Object.freeze([
@ -201,6 +209,114 @@ const AbuseReporter = {
return data; return data;
}, },
/**
* Helper function that opens an abuse report form in a new dialog window.
*
* @param {string} addonId
* The addonId being reported.
* @param {string} reportEntryPoint
* The entry point from which the user has triggered the abuse report
* flow.
* @param {XULElement} browser
* The browser element (if any) that is opening the report window.
*
* @return {Promise<AbuseReportDialog>}
* Returns an AbuseReportDialog object, rejects if it fails to open
* the dialog.
*
* @typedef {object} AbuseReportDialog
* An object that represents the abuse report dialog.
* @prop {function} close
* A method that closes the report dialog (used by the caller
* to close the dialog when the user chooses to close the window
* that started the abuse report flow).
* @prop {Promise<AbuseReport|undefined} promiseReport
* A promise resolved to an AbuseReport instance if the report should
* be submitted, or undefined if the user has cancelled the report.
* Rejects if it fails to create an AbuseReport instance or to open
* the abuse report window.
*/
async openDialog(addonId, reportEntryPoint, browser) {
const chromeWin = browser && browser.ownerGlobal;
if (!chromeWin) {
throw new Error("Abuse Reporter dialog cancelled, opener tab closed");
}
const report = await AbuseReporter.createAbuseReport(addonId, {
reportEntryPoint,
});
const params = Cc["@mozilla.org/array;1"].createInstance(
Ci.nsIMutableArray
);
const dialogInit = {
report,
openWebLink(url) {
chromeWin.openWebLinkIn(url, "tab", {
relatedToCurrent: true,
});
},
};
params.appendElement(dialogInit);
let win;
function closeDialog() {
if (win && !win.closed) {
win.close();
}
}
const promiseReport = new Promise((resolve, reject) => {
dialogInit.deferredReport = { resolve, reject };
}).then(
({ userCancelled }) => {
closeDialog();
return userCancelled ? undefined : report;
},
err => {
Cu.reportError(
`Unexpected abuse report panel error: ${err} :: ${err.stack}`
);
closeDialog();
return Promise.reject({
message: "Unexpected abuse report panel error",
});
}
);
const promiseReportPanel = new Promise((resolve, reject) => {
dialogInit.deferredReportPanel = { resolve, reject };
});
dialogInit.promiseReport = promiseReport;
dialogInit.promiseReportPanel = promiseReportPanel;
win = Services.ww.openWindow(
chromeWin,
"chrome://mozapps/content/extensions/abuse-report-frame.html",
"addons-abuse-report-dialog",
// Set the dialog window options (including a reasonable initial
// window height size, eventually adjusted by the panel once it
// has been rendered its content).
"dialog,centerscreen,alwaysOnTop,height=700",
params
);
return {
close: closeDialog,
promiseReport,
// Properties used in tests
promiseReportPanel,
window: win,
};
},
get openDialogDisabled() {
return !SHOULD_OPEN_DIALOG;
},
}; };
/** /**
@ -234,6 +350,10 @@ class AbuseReport {
addon, addon,
reportData, reportData,
reportEntryPoint, reportEntryPoint,
// message and reason are initially null, and then set by the panel
// using the related set method.
message: null,
reason: null,
}; };
} }
@ -250,21 +370,20 @@ class AbuseReport {
/** /**
* Submit the current report, given a reason and a message. * Submit the current report, given a reason and a message.
* *
* @params {object} options
* @params {string} options.reason
* String identifier for the report reason.
* @params {string} [options.message]
* An optional string which contains a description for the reported issue.
*
* @returns {Promise<void>} * @returns {Promise<void>}
* Resolves once the report has been successfully submitted. * Resolves once the report has been successfully submitted.
* It rejects with an AbuseReportError if the report couldn't be * It rejects with an AbuseReportError if the report couldn't be
* submitted for a known reason (or another Error type otherwise). * submitted for a known reason (or another Error type otherwise).
*/ */
async submit({ reason, message }) { async submit() {
const { aborted, abortController, reportData, reportEntryPoint } = this[ const {
PRIVATE_REPORT_PROPS aborted,
]; abortController,
message,
reason,
reportData,
reportEntryPoint,
} = this[PRIVATE_REPORT_PROPS];
// Record telemetry event and throw an AbuseReportError. // Record telemetry event and throw an AbuseReportError.
const rejectReportError = async (errorType, { response } = {}) => { const rejectReportError = async (errorType, { response } = {}) => {
@ -359,4 +478,24 @@ class AbuseReport {
get reportEntryPoint() { get reportEntryPoint() {
return this[PRIVATE_REPORT_PROPS].reportEntryPoint; return this[PRIVATE_REPORT_PROPS].reportEntryPoint;
} }
/**
* Set the open message (called from the panel when the user submit the report)
*
* @parm {string} message
* An optional string which contains a description for the reported issue.
*/
setMessage(message) {
this[PRIVATE_REPORT_PROPS].message = message;
}
/**
* Set the report reason (called from the panel when the user submit the report)
*
* @parm {string} reason
* String identifier for the report reason.
*/
setReason(reason) {
this[PRIVATE_REPORT_PROPS].reason = reason;
}
} }

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

@ -13,15 +13,20 @@
<link rel="localization" href="toolkit/about/aboutAddons.ftl"> <link rel="localization" href="toolkit/about/aboutAddons.ftl">
<link rel="localization" href="toolkit/about/abuseReports.ftl"> <link rel="localization" href="toolkit/about/abuseReports.ftl">
<script src="chrome://mozapps/content/extensions/abuse-report-panel.js"></script> <script defer src="chrome://mozapps/content/extensions/abuse-report-panel.js"></script>
</head> </head>
<body> <body>
<addon-abuse-report></addon-abuse-report>
<!-- WebComponents Templates --> <!-- WebComponents Templates -->
<template id="tmpl-abuse-report"> <template id="tmpl-modal">
<div class="modal-overlay-outer"></div> <div class="modal-overlay-outer"></div>
<div class="modal-panel-container"> <div class="modal-panel-container"></div>
<form class="card addon-abuse-report" onsubmit="return false;"> </template>
<template id="tmpl-abuse-report">
<form class="addon-abuse-report" onsubmit="return false;">
<div class="abuse-report-header"> <div class="abuse-report-header">
<img class="card-heading-icon addon-icon"/> <img class="card-heading-icon addon-icon"/>
<div class="card-contents"> <div class="card-contents">
@ -58,7 +63,6 @@
</div> </div>
</div> </div>
</form> </form>
</div>
</template> </template>
<template id="tmpl-reasons-panel"> <template id="tmpl-reasons-panel">

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

@ -281,7 +281,18 @@
// abuse report panel from outside of the about:addons page // abuse report panel from outside of the about:addons page
// (e.g. triggered from the browserAction context menu). // (e.g. triggered from the browserAction context menu).
window.openAbuseReport = ({ addonId, reportEntryPoint }) => { window.openAbuseReport = ({ addonId, reportEntryPoint }) => {
if (AbuseReporter.openDialogDisabled) {
const frame = document.querySelector("addon-abuse-report-xulframe"); const frame = document.querySelector("addon-abuse-report-xulframe");
frame.openReport({ addonId, reportEntryPoint }); frame.openReport({ addonId, reportEntryPoint });
return;
}
htmlBrowserLoaded.then(() => {
getHtmlBrowser().contentWindow.openAbuseReport({
addonId,
reportEntryPoint,
});
});
}; };
} }

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

@ -215,3 +215,36 @@ abuse-report-submit-panel textarea {
box-sizing: border-box; box-sizing: border-box;
} }
/* Adapt styles for the panel opened in its own dialog window */
html.dialog-window {
background-color: var(--in-content-box-background);
height: 100%;
min-width: 740px;
}
html.dialog-window body {
overflow: hidden;
min-height: 100%;
display: flex;
flex-direction: column;
}
html.dialog-window .abuse-report-close-icon {
display: none;
}
html.dialog-window addon-abuse-report {
flex-grow: 1;
display: flex;
/* Ensure that the dialog window starts from a reasonable initial size */
--modal-panel-min-width: 700px;
}
html.dialog-window addon-abuse-report form {
display: flex;
}
html.dialog-window addon-abuse-report form .abuse-report-contents {
flex-grow: 1;
}

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

@ -11,6 +11,16 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/Services.jsm" "resource://gre/modules/Services.jsm"
); );
const IS_DIALOG_WINDOW = window.arguments && window.arguments.length;
let openWebLink = IS_DIALOG_WINDOW
? window.arguments[0].wrappedJSObject.openWebLink
: url => {
window.windowRoot.ownerGlobal.openWebLinkIn(url, "tab", {
relatedToCurrent: true,
});
};
const showOnAnyType = () => false; const showOnAnyType = () => false;
const hideOnAnyType = () => true; const hideOnAnyType = () => true;
const hideOnThemeType = addonType => addonType === "theme"; const hideOnThemeType = addonType => addonType === "theme";
@ -449,7 +459,11 @@ class AbuseReport extends HTMLElement {
} }
this.cancel(); this.cancel();
} }
if (!IS_DIALOG_WINDOW) {
// Workaround keyboard navigation issues when
// the panel is running in its own dialog window.
this.handleKeyboardNavigation(evt); this.handleKeyboardNavigation(evt);
}
break; break;
case "click": case "click":
if (evt.target === this._iconClose || evt.target === this._btnCancel) { if (evt.target === this._iconClose || evt.target === this._btnCancel) {
@ -474,9 +488,7 @@ class AbuseReport extends HTMLElement {
const url = evt.target.getAttribute("href"); const url = evt.target.getAttribute("href");
// Ignore if url is empty. // Ignore if url is empty.
if (url) { if (url) {
window.windowRoot.ownerGlobal.openWebLinkIn(url, "tab", { openWebLink(url);
relatedToCurrent: true,
});
} }
} }
break; break;
@ -527,7 +539,23 @@ class AbuseReport extends HTMLElement {
render() { render() {
this.textContent = ""; this.textContent = "";
this.appendChild(document.importNode(this.template.content, true)); const formTemplate = document.importNode(this.template.content, true);
if (IS_DIALOG_WINDOW) {
this.appendChild(formTemplate);
} else {
// Append the report form inside a modal overlay when the report panel
// is a sub-frame of the about:addons tab.
const modalTemplate = document.importNode(
this.modalTemplate.content,
true
);
this.appendChild(modalTemplate);
this.querySelector(".modal-panel-container").appendChild(formTemplate);
// Add the card styles to the form.
this.querySelector("form").classList.add("card");
}
} }
async update() { async update() {
@ -581,6 +609,7 @@ class AbuseReport extends HTMLElement {
_submitPanel.update(); _submitPanel.update();
this.focus(); this.focus();
dispatchCustomEvent(this, "abuse-report:updated", { dispatchCustomEvent(this, "abuse-report:updated", {
addonId, addonId,
panel: "reasons", panel: "reasons",
@ -628,10 +657,10 @@ class AbuseReport extends HTMLElement {
if (!this.isConnected || !this.addon) { if (!this.isConnected || !this.addon) {
return; return;
} }
this._report.setMessage(this.message);
this._report.setReason(this.reason);
dispatchCustomEvent(this, "abuse-report:submit", { dispatchCustomEvent(this, "abuse-report:submit", {
addonId: this.addonId, addonId: this.addonId,
reason: this.reason,
message: this.message,
report: this._report, report: this._report,
}); });
} }
@ -721,6 +750,10 @@ class AbuseReport extends HTMLElement {
return this._form.elements.reason.value; return this._form.elements.reason.value;
} }
get modalTemplate() {
return document.getElementById("tmpl-modal");
}
get template() { get template() {
return document.getElementById("tmpl-abuse-report"); return document.getElementById("tmpl-abuse-report");
} }
@ -737,10 +770,54 @@ customElements.define("abuse-report-reasons-panel", AbuseReasonsPanel);
customElements.define("abuse-report-submit-panel", AbuseSubmitPanel); customElements.define("abuse-report-submit-panel", AbuseSubmitPanel);
customElements.define("addon-abuse-report", AbuseReport); customElements.define("addon-abuse-report", AbuseReport);
window.addEventListener( // The panel has been opened in a new dialog window.
"load", if (IS_DIALOG_WINDOW) {
// CSS customizations when panel is in its own window
// (vs. being an about:addons subframe).
document.documentElement.className = "dialog-window";
const {
report,
deferredReport,
deferredReportPanel,
} = window.arguments[0].wrappedJSObject;
const el = document.querySelector("addon-abuse-report");
el.addEventListener("abuse-report:submit", () => {
deferredReport.resolve({
userCancelled: false,
report,
});
});
el.addEventListener(
"abuse-report:cancel",
() => { () => {
document.body.prepend(document.createElement("addon-abuse-report")); deferredReport.resolve({ userCancelled: true });
}, },
{ once: true } { once: true }
); );
// Adjust window size (if needed) once the fluent strings have been
// added to the document and the document has been flushed.
el.addEventListener(
"abuse-report:updated",
async () => {
const form = document.querySelector("form");
await document.l10n.translateFragment(form);
const { clientWidth, clientHeight } = await window.promiseDocumentFlushed(
() => form
);
// Resolve promiseReportPanel once the panel completed the initial render
// (used in tests).
deferredReportPanel.resolve(el);
if (
window.innerWidth !== clientWidth ||
window.innerheight !== clientHeight
) {
window.resizeTo(clientWidth, clientHeight);
}
},
{ once: true }
);
el.setAbuseReport(report);
}

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

@ -11,6 +11,10 @@
* helpers used for the Abuse Reporting submission (and related message bars). * helpers used for the Abuse Reporting submission (and related message bars).
*/ */
const { AbuseReporter } = ChromeUtils.import(
"resource://gre/modules/AbuseReporter.jsm"
);
// Message Bars definitions. // Message Bars definitions.
const ABUSE_REPORT_MESSAGE_BARS = { const ABUSE_REPORT_MESSAGE_BARS = {
// Idle message-bar (used while the submission is still ongoing). // Idle message-bar (used while the submission is still ongoing).
@ -73,14 +77,66 @@ const ABUSE_REPORT_MESSAGE_BARS = {
}, },
}; };
function openAbuseReport({ addonId, reportEntryPoint }) { async function openAbuseReport({ addonId, reportEntryPoint }) {
if (AbuseReporter.openDialogDisabled) {
document.dispatchEvent( document.dispatchEvent(
new CustomEvent("abuse-report:new", { new CustomEvent("abuse-report:new", {
detail: { addonId, reportEntryPoint }, detail: { addonId, reportEntryPoint },
}) })
); );
return;
} }
try {
const reportDialog = await AbuseReporter.openDialog(
addonId,
reportEntryPoint,
window.docShell.chromeEventHandler
);
// Warn the user before the about:addons tab while an
// abuse report dialog is still open, and close the
// report dialog if the user choose to close the related
// about:addons tab.
const beforeunloadListener = evt => evt.preventDefault();
const unloadListener = () => reportDialog.close();
const clearUnloadListeners = () => {
window.removeEventListener("beforeunload", beforeunloadListener);
window.removeEventListener("unload", unloadListener);
};
window.addEventListener("beforeunload", beforeunloadListener);
window.addEventListener("unload", unloadListener);
reportDialog.promiseReport
.then(
report => {
if (report) {
submitReport({ report });
}
},
err => {
Cu.reportError(
`Unexpected abuse report panel error: ${err} :: ${err.stack}`
);
reportDialog.close();
}
)
.then(clearUnloadListeners);
} catch (err) {
document.dispatchEvent(
new CustomEvent("abuse-report:create-error", {
detail: {
addonId,
addon: err.addon,
errorType: err.errorType,
},
})
);
}
}
window.openAbuseReport = openAbuseReport;
// Helper function used to create abuse report message bars in the // Helper function used to create abuse report message bars in the
// HTML about:addons page. // HTML about:addons page.
function createReportMessageBar( function createReportMessageBar(
@ -143,12 +199,20 @@ function createReportMessageBar(
return messagebar; return messagebar;
} }
async function submitReport({ report, reason, message }) { async function submitReport({ report }) {
const { addon } = report; const { addon } = report;
const addonId = addon.id; const addonId = addon.id;
const addonName = addon.name; const addonName = addon.name;
const addonType = addon.type; const addonType = addon.type;
// Ensure that the tab that originated the report dialog is selected
// when the user is submitting the report.
const { gBrowser } = window.windowRoot.ownerGlobal;
if (gBrowser && gBrowser.getTabForBrowser) {
let tab = gBrowser.getTabForBrowser(window.docShell.chromeEventHandler);
gBrowser.selectedTab = tab;
}
// Create a message bar while we are still submitting the report. // Create a message bar while we are still submitting the report.
const mbSubmitting = createReportMessageBar( const mbSubmitting = createReportMessageBar(
"submitting", "submitting",
@ -164,7 +228,7 @@ async function submitReport({ report, reason, message }) {
); );
try { try {
await report.submit({ reason, message }); await report.submit();
mbSubmitting.remove(); mbSubmitting.remove();
// Create a submitted message bar when the submission has been // Create a submitted message bar when the submission has been
@ -227,7 +291,7 @@ async function submitReport({ report, reason, message }) {
mbError.remove(); mbError.remove();
switch (action) { switch (action) {
case "retry": case "retry":
submitReport({ report, reason, message }); submitReport({ report });
break; break;
case "cancel": case "cancel":
report.abort(); report.abort();

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

@ -773,10 +773,11 @@ add_task(async function test_abusereport_submit() {
const expectedDetail = { const expectedDetail = {
addonId: extension.id, addonId: extension.id,
reason: abuseReportEl.reason,
message: abuseReportEl.message,
}; };
const expectedReason = abuseReportEl.reason;
const expectedMessage = abuseReportEl.message;
let reportSubmitted; let reportSubmitted;
const onReportSubmitted = new Promise(resolve => { const onReportSubmitted = new Promise(resolve => {
apiRequestHandler = ({ data, request, response }) => { apiRequestHandler = ({ data, request, response }) => {
@ -802,8 +803,6 @@ add_task(async function test_abusereport_submit() {
const actualDetail = { const actualDetail = {
addonId: submitEvent.detail.addonId, addonId: submitEvent.detail.addonId,
reason: submitEvent.detail.reason,
message: submitEvent.detail.message,
}; };
Assert.deepEqual( Assert.deepEqual(
actualDetail, actualDetail,
@ -838,12 +837,12 @@ add_task(async function test_abusereport_submit() {
); );
is( is(
reportSubmitted.reason, reportSubmitted.reason,
expectedDetail.reason, expectedReason,
"Got the expected reason in the submitted report" "Got the expected reason in the submitted report"
); );
is( is(
reportSubmitted.message, reportSubmitted.message,
expectedDetail.message, expectedMessage,
"Got the expected message in the submitted report" "Got the expected message in the submitted report"
); );
is( is(

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

@ -325,7 +325,9 @@ add_task(async function test_report_create_and_submit() {
}; };
info("Submitting report"); info("Submitting report");
await report.submit(reportProperties); report.setMessage(reportProperties.message);
report.setReason(reportProperties.reason);
await report.submit();
const expectedEntries = Object.entries({ const expectedEntries = Object.entries({
report_entry_point: reportEntryPoint, report_entry_point: reportEntryPoint,
@ -382,11 +384,10 @@ add_task(async function test_error_recent_submit() {
); );
// Submit the two reports in fast sequence. // Submit the two reports in fast sequence.
await report.submit({ reason: "reason1" }); report.setReason("reason1");
await assertRejectsAbuseReportError( report2.setReason("reason2");
report2.submit({ reason: "reason2" }), await report.submit();
"ERROR_RECENT_SUBMIT" await assertRejectsAbuseReportError(report2.submit(), "ERROR_RECENT_SUBMIT");
);
equal( equal(
reportSubmitted.reason, reportSubmitted.reason,
"reason1", "reason1",
@ -443,7 +444,8 @@ add_task(async function test_submission_server_error() {
ADDON_ID, ADDON_ID,
REPORT_OPTIONS REPORT_OPTIONS
); );
const promiseSubmit = report.submit({ reason: "a-reason" }); report.setReason("a-reason");
const promiseSubmit = report.submit();
if (typeof expectedErrorType === "string") { if (typeof expectedErrorType === "string") {
// Assert a specific AbuseReportError errorType. // Assert a specific AbuseReportError errorType.
await assertRejectsAbuseReportError( await assertRejectsAbuseReportError(
@ -565,7 +567,8 @@ add_task(async function test_submission_aborting() {
ADDON_ID, ADDON_ID,
REPORT_OPTIONS REPORT_OPTIONS
); );
const promiseResult = report.submit({ reason: "a-reason" }); report.setReason("a-reason");
const promiseResult = report.submit();
await onRequestReceived; await onRequestReceived;
@ -624,7 +627,9 @@ add_task(async function test_truncated_string_properties() {
REPORT_OPTIONS REPORT_OPTIONS
); );
await report.submit({ message: "fake-message", reason: "fake-reason" }); report.setMessage("fake-message");
report.setReason("fake-reason");
await report.submit();
const expected = { const expected = {
addon_name: generateString(255), addon_name: generateString(255),
@ -686,7 +691,9 @@ add_task(async function test_report_recommended() {
addonId, addonId,
REPORT_OPTIONS REPORT_OPTIONS
); );
await report.submit({ message: "fake-message", reason: "fake-reason" }); report.setMessage("fake-message");
report.setReason("fake-reason");
await report.submit();
equal( equal(
reportSubmitted.addon_signature, reportSubmitted.addon_signature,
expectedAddonSignature, expectedAddonSignature,