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("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/");
// Blocklist preferences

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

@ -11,6 +11,7 @@ const { XPCOMUtils } = ChromeUtils.import(
Cu.importGlobalProperties(["fetch"]);
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.
const MAX_STRING_LENGTH = 255;
@ -32,6 +33,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
PREF_ABUSE_REPORT_URL
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"SHOULD_OPEN_DIALOG",
PREF_ABUSE_REPORT_OPEN_DIALOG,
false
);
const PRIVATE_REPORT_PROPS = Symbol("privateReportProps");
const ERROR_TYPES = Object.freeze([
@ -201,6 +209,114 @@ const AbuseReporter = {
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,
reportData,
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.
*
* @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>}
* Resolves once the report has been successfully submitted.
* It rejects with an AbuseReportError if the report couldn't be
* submitted for a known reason (or another Error type otherwise).
*/
async submit({ reason, message }) {
const { aborted, abortController, reportData, reportEntryPoint } = this[
PRIVATE_REPORT_PROPS
];
async submit() {
const {
aborted,
abortController,
message,
reason,
reportData,
reportEntryPoint,
} = this[PRIVATE_REPORT_PROPS];
// Record telemetry event and throw an AbuseReportError.
const rejectReportError = async (errorType, { response } = {}) => {
@ -359,4 +478,24 @@ class AbuseReport {
get 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,52 +13,56 @@
<link rel="localization" href="toolkit/about/aboutAddons.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>
<body>
<addon-abuse-report></addon-abuse-report>
<!-- WebComponents Templates -->
<template id="tmpl-abuse-report">
<template id="tmpl-modal">
<div class="modal-overlay-outer"></div>
<div class="modal-panel-container">
<form class="card addon-abuse-report" onsubmit="return false;">
<div class="abuse-report-header">
<img class="card-heading-icon addon-icon"/>
<div class="card-contents">
<span class="addon-name"></span>
<span class="addon-author-box"
data-l10n-args='{"author-name": "author placeholder"}'
data-l10n-id="abuse-report-addon-authored-by">
<a data-l10n-name="author-name"
class="author" href="#" target="_blank"></a>
</span>
</div>
<div class="modal-panel-container"></div>
</template>
<template id="tmpl-abuse-report">
<form class="addon-abuse-report" onsubmit="return false;">
<div class="abuse-report-header">
<img class="card-heading-icon addon-icon"/>
<div class="card-contents">
<span class="addon-name"></span>
<span class="addon-author-box"
data-l10n-args='{"author-name": "author placeholder"}'
data-l10n-id="abuse-report-addon-authored-by">
<a data-l10n-name="author-name"
class="author" href="#" target="_blank"></a>
</span>
</div>
<button class="abuse-report-close-icon"></button>
<div class="abuse-report-contents">
<abuse-report-reasons-panel></abuse-report-reasons-panel>
<abuse-report-submit-panel hidden></abuse-report-submit-panel>
</div>
<button class="abuse-report-close-icon"></button>
<div class="abuse-report-contents">
<abuse-report-reasons-panel></abuse-report-reasons-panel>
<abuse-report-submit-panel hidden></abuse-report-submit-panel>
</div>
<div class="abuse-report-buttons">
<div class="abuse-report-reasons-buttons">
<button class="abuse-report-cancel"
data-l10n-id="abuse-report-cancel-button">
</button>
<button class="primary abuse-report-next"
data-l10n-id="abuse-report-next-button">
</button>
</div>
<div class="abuse-report-buttons">
<div class="abuse-report-reasons-buttons">
<button class="abuse-report-cancel"
data-l10n-id="abuse-report-cancel-button">
</button>
<button class="primary abuse-report-next"
data-l10n-id="abuse-report-next-button">
</button>
</div>
<div class="abuse-report-submit-buttons" hidden>
<button class="abuse-report-goback"
data-l10n-id="abuse-report-goback-button">
</button>
<button class="primary abuse-report-submit"
data-l10n-id="abuse-report-submit-button">
</button>
</div>
<div class="abuse-report-submit-buttons" hidden>
<button class="abuse-report-goback"
data-l10n-id="abuse-report-goback-button">
</button>
<button class="primary abuse-report-submit"
data-l10n-id="abuse-report-submit-button">
</button>
</div>
</form>
</div>
</div>
</form>
</template>
<template id="tmpl-reasons-panel">

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

@ -281,7 +281,18 @@
// abuse report panel from outside of the about:addons page
// (e.g. triggered from the browserAction context menu).
window.openAbuseReport = ({ addonId, reportEntryPoint }) => {
const frame = document.querySelector("addon-abuse-report-xulframe");
frame.openReport({ addonId, reportEntryPoint });
if (AbuseReporter.openDialogDisabled) {
const frame = document.querySelector("addon-abuse-report-xulframe");
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;
}
/* 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"
);
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 hideOnAnyType = () => true;
const hideOnThemeType = addonType => addonType === "theme";
@ -449,7 +459,11 @@ class AbuseReport extends HTMLElement {
}
this.cancel();
}
this.handleKeyboardNavigation(evt);
if (!IS_DIALOG_WINDOW) {
// Workaround keyboard navigation issues when
// the panel is running in its own dialog window.
this.handleKeyboardNavigation(evt);
}
break;
case "click":
if (evt.target === this._iconClose || evt.target === this._btnCancel) {
@ -474,9 +488,7 @@ class AbuseReport extends HTMLElement {
const url = evt.target.getAttribute("href");
// Ignore if url is empty.
if (url) {
window.windowRoot.ownerGlobal.openWebLinkIn(url, "tab", {
relatedToCurrent: true,
});
openWebLink(url);
}
}
break;
@ -527,7 +539,23 @@ class AbuseReport extends HTMLElement {
render() {
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() {
@ -581,6 +609,7 @@ class AbuseReport extends HTMLElement {
_submitPanel.update();
this.focus();
dispatchCustomEvent(this, "abuse-report:updated", {
addonId,
panel: "reasons",
@ -628,10 +657,10 @@ class AbuseReport extends HTMLElement {
if (!this.isConnected || !this.addon) {
return;
}
this._report.setMessage(this.message);
this._report.setReason(this.reason);
dispatchCustomEvent(this, "abuse-report:submit", {
addonId: this.addonId,
reason: this.reason,
message: this.message,
report: this._report,
});
}
@ -721,6 +750,10 @@ class AbuseReport extends HTMLElement {
return this._form.elements.reason.value;
}
get modalTemplate() {
return document.getElementById("tmpl-modal");
}
get template() {
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("addon-abuse-report", AbuseReport);
window.addEventListener(
"load",
() => {
document.body.prepend(document.createElement("addon-abuse-report"));
},
{ once: true }
);
// The panel has been opened in a new dialog window.
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",
() => {
deferredReport.resolve({ userCancelled: 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).
*/
const { AbuseReporter } = ChromeUtils.import(
"resource://gre/modules/AbuseReporter.jsm"
);
// Message Bars definitions.
const ABUSE_REPORT_MESSAGE_BARS = {
// Idle message-bar (used while the submission is still ongoing).
@ -73,14 +77,66 @@ const ABUSE_REPORT_MESSAGE_BARS = {
},
};
function openAbuseReport({ addonId, reportEntryPoint }) {
document.dispatchEvent(
new CustomEvent("abuse-report:new", {
detail: { addonId, reportEntryPoint },
})
);
async function openAbuseReport({ addonId, reportEntryPoint }) {
if (AbuseReporter.openDialogDisabled) {
document.dispatchEvent(
new CustomEvent("abuse-report:new", {
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
// HTML about:addons page.
function createReportMessageBar(
@ -143,12 +199,20 @@ function createReportMessageBar(
return messagebar;
}
async function submitReport({ report, reason, message }) {
async function submitReport({ report }) {
const { addon } = report;
const addonId = addon.id;
const addonName = addon.name;
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.
const mbSubmitting = createReportMessageBar(
"submitting",
@ -164,7 +228,7 @@ async function submitReport({ report, reason, message }) {
);
try {
await report.submit({ reason, message });
await report.submit();
mbSubmitting.remove();
// Create a submitted message bar when the submission has been
@ -227,7 +291,7 @@ async function submitReport({ report, reason, message }) {
mbError.remove();
switch (action) {
case "retry":
submitReport({ report, reason, message });
submitReport({ report });
break;
case "cancel":
report.abort();

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

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

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

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