Bug 648675 - Allow comments and URL opt-in in content/plugin crash UI. r=dolske,ted

This commit is contained in:
Drew Willcoxon 2013-02-14 15:57:50 -08:00
parent 2d7aa1784a
commit 8715a08802
13 changed files with 384 additions and 71 deletions

View File

@ -8,6 +8,11 @@ var gPluginHandler = {
PLUGIN_SCRIPTED_STATE_FIRED: 1,
PLUGIN_SCRIPTED_STATE_DONE: 2,
getPluginUI: function (plugin, className) {
return plugin.ownerDocument.
getAnonymousElementByAttribute(plugin, "class", className);
},
#ifdef MOZ_CRASHREPORTER
get CrashSubmit() {
delete this.CrashSubmit;
@ -409,11 +414,16 @@ var gPluginHandler = {
},
#ifdef MOZ_CRASHREPORTER
// Callback for user clicking "submit a report" link
submitReport : function(pluginDumpID, browserDumpID) {
// The crash reporter wants a DOM element it can append an IFRAME to,
// which it uses to submit a form. Let's just give it gBrowser.
this.CrashSubmit.submit(pluginDumpID);
submitReport: function submitReport(pluginDumpID, browserDumpID, plugin) {
let keyVals = {};
if (plugin) {
let userComment = this.getPluginUI(plugin, "submitComment").value.trim();
if (userComment)
keyVals.PluginUserComment = userComment;
if (this.getPluginUI(plugin, "submitURLOptIn").checked)
keyVals.PluginContentURL = plugin.ownerDocument.URL;
}
this.CrashSubmit.submit(pluginDumpID, { extraExtraKeyVals: keyVals });
if (browserDumpID)
this.CrashSubmit.submit(browserDumpID);
},
@ -937,11 +947,16 @@ var gPluginHandler = {
}
else { // doPrompt
status = "please";
// XXX can we make the link target actually be blank?
let pleaseLink = doc.getAnonymousElementByAttribute(
plugin, "class", "pleaseSubmitLink");
this.addLinkClickCallback(pleaseLink, "submitReport",
pluginDumpID, browserDumpID);
this.getPluginUI(plugin, "submitButton").addEventListener("click",
function (event) {
if (event.button != 0 || !event.isTrusted)
return;
this.submitReport(pluginDumpID, browserDumpID, plugin);
pref.setBoolPref("", optInCB.checked);
}.bind(this));
let optInCB = this.getPluginUI(plugin, "submitURLOptIn");
let pref = Services.prefs.getBranch("dom.ipc.plugins.reportCrashURL");
optInCB.checked = pref.getBoolPref("");
}
// If we don't have a minidumpID, we can't (or didn't) submit anything.
@ -952,8 +967,6 @@ var gPluginHandler = {
statusDiv.setAttribute("status", status);
let bottomLinks = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgBottomLinks");
bottomLinks.style.display = "block";
let helpIcon = doc.getAnonymousElementByAttribute(plugin, "class", "helpIcon");
this.addLinkClickCallback(helpIcon, "openHelpPage");
@ -990,7 +1003,7 @@ var gPluginHandler = {
}
#endif
let crashText = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgCrashed");
let crashText = doc.getAnonymousElementByAttribute(plugin, "class", "msgCrashedText");
crashText.textContent = messageString;
let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document);
@ -1000,21 +1013,32 @@ var gPluginHandler = {
let notificationBox = gBrowser.getNotificationBox(browser);
let isShowing = true;
// Is the <object>'s size too small to hold what we want to show?
if (this.isTooSmall(plugin, overlay)) {
// Hide the overlay's contents. Use visibility style, so that it
// doesn't collapse down to 0x0.
// First try hiding the crash report submission UI.
statusDiv.removeAttribute("status");
if (this.isTooSmall(plugin, overlay)) {
// Hide the overlay's contents. Use visibility style, so that it doesn't
// collapse down to 0x0.
overlay.style.visibility = "hidden";
// If another plugin on the page was large enough to show our UI, we
// don't want to show a notification bar.
if (!doc.mozNoPluginCrashedNotification)
showNotificationBar(pluginDumpID, browserDumpID);
isShowing = false;
}
}
if (isShowing) {
// If a previous plugin on the page was too small and resulted in adding a
// notification bar, then remove it because this plugin instance it big
// enough to serve as in-content notification.
hideNotificationBar();
doc.mozNoPluginCrashedNotification = true;
} else {
// If a previous plugin on the page was too small and resulted in
// adding a notification bar, then remove it because this plugin
// instance it big enough to serve as in-content notification.
hideNotificationBar();
doc.mozNoPluginCrashedNotification = true;
// If another plugin on the page was large enough to show our UI, we don't
// want to show a notification bar.
if (!doc.mozNoPluginCrashedNotification)
showNotificationBar(pluginDumpID, browserDumpID);
}
function hideNotificationBar() {

View File

@ -302,6 +302,8 @@ _BROWSER_FILES = \
browser_pageInfo_plugins.js \
browser_pageInfo.js \
feed_tab.html \
browser_pluginCrashCommentAndURL.js \
pluginCrashCommentAndURL.html \
$(NULL)
ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))

View File

@ -0,0 +1,154 @@
/* 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/. */
Cu.import("resource://gre/modules/Services.jsm");
const CRASH_URL = "http://example.com/browser/browser/base/content/test/pluginCrashCommentAndURL.html";
const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs";
function test() {
// Crashing the plugin takes up a lot of time, so extend the test timeout.
requestLongerTimeout(runs.length);
waitForExplicitFinish();
// The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables plugin
// crash reports. This test needs them enabled. The test also needs a mock
// report server, and fortunately one is already set up by toolkit/
// crashreporter/test/Makefile.in. Assign its URL to MOZ_CRASHREPORTER_URL,
// which CrashSubmit.jsm uses as a server override.
let env = Cc["@mozilla.org/process/environment;1"].
getService(Components.interfaces.nsIEnvironment);
let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT");
let serverURL = env.get("MOZ_CRASHREPORTER_URL");
env.set("MOZ_CRASHREPORTER_NO_REPORT", "");
env.set("MOZ_CRASHREPORTER_URL", SERVER_URL);
let tab = gBrowser.loadOneTab("about:blank", { inBackground: false });
let browser = gBrowser.getBrowserForTab(tab);
browser.addEventListener("PluginCrashed", onCrash, false);
Services.obs.addObserver(onSubmitStatus, "crash-report-status", false);
registerCleanupFunction(function cleanUp() {
env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
env.set("MOZ_CRASHREPORTER_URL", serverURL);
gBrowser.selectedBrowser.removeEventListener("PluginCrashed", onCrash,
false);
Services.obs.removeObserver(onSubmitStatus, "crash-report-status");
gBrowser.removeCurrentTab();
});
doNextRun();
}
let runs = [
{
shouldSubmissionUIBeVisible: true,
comment: "",
urlOptIn: false,
},
{
shouldSubmissionUIBeVisible: true,
comment: "a test comment",
urlOptIn: true,
},
{
width: 300,
height: 300,
shouldSubmissionUIBeVisible: false,
},
];
let currentRun = null;
function doNextRun() {
try {
if (!runs.length) {
finish();
return;
}
currentRun = runs.shift();
let args = ["width", "height"].reduce(function (memo, arg) {
if (arg in currentRun)
memo[arg] = currentRun[arg];
return memo;
}, {});
gBrowser.loadURI(CRASH_URL + "?" +
encodeURIComponent(JSON.stringify(args)));
// And now wait for the crash.
}
catch (err) {
failWithException(err);
finish();
}
}
function onCrash() {
try {
let plugin = gBrowser.contentDocument.getElementById("plugin");
let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin);
let style =
gBrowser.contentWindow.getComputedStyle(elt("msg msgPleaseSubmit"));
is(style.display,
currentRun.shouldSubmissionUIBeVisible ? "block" : "none",
"Submission UI visibility should be correct");
if (!currentRun.shouldSubmissionUIBeVisible) {
// Done with this run.
doNextRun();
return;
}
elt("submitComment").value = currentRun.comment;
elt("submitURLOptIn").checked = currentRun.urlOptIn;
elt("submitButton").click();
// And now wait for the submission status notification.
}
catch (err) {
failWithException(err);
doNextRun();
}
}
function onSubmitStatus(subj, topic, data) {
try {
// Wait for success or failed, doesn't matter which.
if (data != "success" && data != "failed")
return;
let extra = getPropertyBagValue(subj.QueryInterface(Ci.nsIPropertyBag),
"extra");
ok(extra instanceof Ci.nsIPropertyBag, "Extra data should be property bag");
let val = getPropertyBagValue(extra, "PluginUserComment");
if (currentRun.comment)
is(val, currentRun.comment,
"Comment in extra data should match comment in textbox");
else
ok(val === undefined,
"Comment should be absent from extra data when textbox is empty");
val = getPropertyBagValue(extra, "PluginContentURL");
if (currentRun.urlOptIn)
is(val, gBrowser.currentURI.spec,
"URL in extra data should match browser URL when opt-in checked");
else
ok(val === undefined,
"URL should be absent from extra data when opt-in not checked");
}
catch (err) {
failWithException(err);
}
doNextRun();
}
function getPropertyBagValue(bag, key) {
try {
var val = bag.getProperty(key);
}
catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
return val;
}
function failWithException(err) {
ok(false, "Uncaught exception: " + err + "\n" + err.stack);
}

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript">
function crash() {
var plugin = document.getElementById("plugin");
var argStr = decodeURIComponent(window.location.search.substr(1));
if (argStr) {
var args = JSON.parse(argStr);
for (var key in args)
plugin.setAttribute(key, args[key]);
}
try {
plugin.crash();
}
catch (err) {}
}
</script>
</head>
<body onload="crash();">
<embed id="plugin" type="application/x-test"
width="400" height="400"
drawmode="solid" color="FF00FFFF">
</embed>
</body>
</html>

View File

@ -86,10 +86,11 @@ function onPluginCrashed(aEvent) {
ok(true, "Plugin crashed notification received");
is(aEvent.type, "PluginCrashed", "event is correct type");
let pleaseLink = document.getAnonymousElementByAttribute(
aEvent.target, "class", "pleaseSubmitLink");
let submitButton = document.getAnonymousElementByAttribute(aEvent.target,
"class",
"submitButton");
// try to submit this report
sendMouseEvent({type:'click'}, pleaseLink, window);
sendMouseEvent({type:'click'}, submitButton, window);
}
function runTests() {

View File

@ -112,10 +112,11 @@ function onPluginCrashed(aEvent) {
ok(true, "Plugin crashed notification received");
is(aEvent.type, "PluginCrashed", "event is correct type");
let pleaseLink = document.getAnonymousElementByAttribute(
aEvent.target, "class", "pleaseSubmitLink");
let submitButton = document.getAnonymousElementByAttribute(aEvent.target,
"class",
"submitButton");
// try to submit this report
sendMouseEvent({type:'click'}, pleaseLink, window);
sendMouseEvent({type:'click'}, submitButton, window);
}
function runTests() {

View File

@ -1815,6 +1815,7 @@ pref("dom.ipc.plugins.java.enabled", false);
#endif
pref("dom.ipc.plugins.flash.subprocess.crashreporter.enabled", true);
pref("dom.ipc.plugins.reportCrashURL", true);
pref("dom.ipc.processCount", 1);

View File

@ -186,12 +186,14 @@ function writeSubmittedReport(crashID, viewURL) {
}
// the Submitter class represents an individual submission.
function Submitter(id, submitSuccess, submitError, noThrottle) {
function Submitter(id, submitSuccess, submitError, noThrottle,
extraExtraKeyVals) {
this.id = id;
this.successCallback = submitSuccess;
this.errorCallback = submitError;
this.noThrottle = noThrottle;
this.additionalDumps = [];
this.extraKeyVals = extraExtraKeyVals || {};
}
Submitter.prototype = {
@ -238,13 +240,10 @@ Submitter.prototype = {
submitForm: function Submitter_submitForm()
{
let reportData = parseKeyValuePairsFromFile(this.extra);
if (!('ServerURL' in reportData)) {
if (!('ServerURL' in this.extraKeyVals)) {
return false;
}
let serverURL = reportData.ServerURL;
delete reportData.ServerURL;
let serverURL = this.extraKeyVals.ServerURL;
// Override the submission URL from the environment or prefs.
@ -253,7 +252,7 @@ Submitter.prototype = {
if (envOverride != '') {
serverURL = envOverride;
}
else if ('PluginHang' in reportData) {
else if ('PluginHang' in this.extraKeyVals) {
try {
serverURL = Services.prefs.
getCharPref("toolkit.crashreporter.pluginHangSubmitURL");
@ -266,9 +265,11 @@ Submitter.prototype = {
let formData = Cc["@mozilla.org/files/formdata;1"]
.createInstance(Ci.nsIDOMFormData);
// add the other data
for (let [name, value] in Iterator(reportData)) {
formData.append(name, value);
// add the data
for (let [name, value] in Iterator(this.extraKeyVals)) {
if (name != "ServerURL") {
formData.append(name, value);
}
}
if (this.noThrottle) {
// tell the server not to throttle this, since it was manually submitted
@ -311,6 +312,13 @@ Submitter.prototype = {
propBag.setPropertyAsAString("serverCrashID", ret.CrashID);
}
let extraKeyValsBag = Cc["@mozilla.org/hash-property-bag;1"].
createInstance(Ci.nsIWritablePropertyBag2);
for (let key in this.extraKeyVals) {
extraKeyValsBag.setPropertyAsAString(key, this.extraKeyVals[key]);
}
propBag.setPropertyAsInterface("extra", extraKeyValsBag);
Services.obs.notifyObservers(propBag, "crash-report-status", status);
switch (status) {
@ -336,10 +344,16 @@ Submitter.prototype = {
return false;
}
let reportData = parseKeyValuePairsFromFile(extra);
let extraKeyVals = parseKeyValuePairsFromFile(extra);
for (let key in extraKeyVals) {
if (!(key in this.extraKeyVals)) {
this.extraKeyVals[key] = extraKeyVals[key];
}
}
let additionalDumps = [];
if ("additional_minidumps" in reportData) {
let names = reportData.additional_minidumps.split(',');
if ("additional_minidumps" in this.extraKeyVals) {
let names = this.extraKeyVals.additional_minidumps.split(',');
for (let name of names) {
let [dump, extra] = getPendingMinidump(this.id + "-" + name);
if (!dump.exists()) {
@ -391,6 +405,11 @@ this.CrashSubmit = {
* it should be processed right away. This should be set
* when the report is being submitted and the user expects
* to see the results immediately. Defaults to false.
* - extraExtraKeyVals
* An object whose key-value pairs will be merged with the data from
* the ".extra" file submitted with the report. The properties of
* this object will override properties of the same name in the
* .extra file.
*
* @return true if the submission began successfully, or false if
* it failed for some reason. (If the dump file does not
@ -402,6 +421,7 @@ this.CrashSubmit = {
let submitSuccess = null;
let submitError = null;
let noThrottle = false;
let extraExtraKeyVals = null;
if ('submitSuccess' in params)
submitSuccess = params.submitSuccess;
@ -409,11 +429,14 @@ this.CrashSubmit = {
submitError = params.submitError;
if ('noThrottle' in params)
noThrottle = params.noThrottle;
if ('extraExtraKeyVals' in params)
extraExtraKeyVals = params.extraExtraKeyVals;
let submitter = new Submitter(id,
submitSuccess,
submitError,
noThrottle);
noThrottle,
extraExtraKeyVals);
CrashSubmit._activeSubmissions.push(submitter);
return submitter.submit();
},

View File

@ -56,6 +56,8 @@
<!ENTITY report.disabled "Crash reporting disabled.">
<!ENTITY report.failed "Submission failed.">
<!ENTITY report.unavailable "No report available.">
<!ENTITY report.comment "Add a comment (comments are publicly visible)">
<!ENTITY report.pageURL "Include the page's URL">
<!ENTITY plugin.file "File">
<!ENTITY plugin.mimeTypes "MIME Types">

View File

@ -37,27 +37,36 @@
<html:div class="msg msgClickToPlay">&clickToActivatePlugin;</html:div>
<html:div class="msg msgDisabled">&disabledPlugin;</html:div>
<html:div class="msg msgBlocked">&blockedPlugin.label;</html:div>
<html:div class="msg msgCrashed"><!-- set at runtime --></html:div>
<html:div class="msg msgCrashed">
<html:div class="msgCrashedText"><!-- set at runtime --></html:div>
<!-- link href set at runtime -->
<html:div class="msgReload">&reloadPlugin.pre;<html:a class="reloadLink" href="">&reloadPlugin.middle;</html:a>&reloadPlugin.post;</html:div>
</html:div>
<html:div class="installStatus">
<html:div class="msg msgInstallPlugin"><html:a class="installPluginLink" href="">&installPlugin;</html:a></html:div>
</html:div>
<html:div class="msg msgManagePlugins"><html:a class="managePluginsLink" href="">&managePlugins;</html:a></html:div>
<html:div class="submitStatus">
<!-- links set at runtime -->
<html:div class="msg msgPleaseSubmit"><html:a class="pleaseSubmitLink" href="">&report.please;</html:a></html:div>
<html:div class="msg msgPleaseSubmit">
<html:textarea class="submitComment"
placeholder="&report.comment;"/>
<html:div class="submitURLOptInBox">
<html:label><html:input class="submitURLOptIn" type="checkbox"/> &report.pageURL;</html:label>
</html:div>
<html:div class="submitButtonBox">
<html:span class="helpIcon" role="link"/>
<html:input class="submitButton" type="button"
value="&report.please;"/>
</html:div>
</html:div>
<html:div class="msg msgSubmitting">&report.submitting;<html:span class="throbber"> </html:span></html:div>
<html:div class="msg msgSubmitted">&report.submitted;</html:div>
<html:div class="msg msgNotSubmitted">&report.disabled;</html:div>
<html:div class="msg msgSubmitFailed">&report.failed;</html:div>
<html:div class="msg msgNoCrashReport">&report.unavailable;</html:div>
<!-- link href set at runtime -->
<html:div class="msg msgReload">&reloadPlugin.pre;<html:a class="reloadLink" href="">&reloadPlugin.middle;</html:a>&reloadPlugin.post;</html:div>
</html:div>
<xul:spacer flex="1"/>
<html:div class="msg msgBottomLinks">
<html:span class="helpIcon" role="link"/>
</html:div>
</xul:vbox>
</xul:vbox>
<html:div class="previewPluginContent"><!-- iframe and its src will be set at runtime --></html:div>

View File

@ -104,21 +104,18 @@ html|applet:not([height]), html|applet[height=""] {
display: block;
}
.submitStatus[status] {
display: -moz-box;
-moz-box-align: center;
-moz-box-pack: center;
height: 160px;
}
.submitStatus[status="noReport"] .msgNoCrashReport,
.submitStatus[status="please"] .msgPleaseSubmit,
.submitStatus[status="noSubmit"] .msgNotSubmitted,
.submitStatus[status="submitting"] .msgSubmitting,
.submitStatus[status="success"] .msgSubmitted,
.submitStatus[status="failed"] .msgSubmitFailed,
.submitStatus[status]:not([status="please"]) .msgReload {
.submitStatus[status="failed"] .msgSubmitFailed {
display: block;
}
.submitStatus[status="please"] .msgReload {
/* Take up space when invisible, so stuff doesn't shift upon reveal. */
display: block;
visibility: hidden;
}
.helpIcon {
cursor: pointer;
}

View File

@ -100,17 +100,53 @@ html|a {
min-height: 19px; /* height of biggest line (with throbber) */
}
.msgBottomLinks {
padding-left: 2px;
padding-right: 2px;
.submitComment {
width: 340px;
height: 70px;
padding: 5px;
border: none;
border-radius: 5px;
resize: none;
font-family: inherit;
font-size: inherit;
}
.submitURLOptInBox {
text-align: start;
}
.submitURLOptIn {
margin-left: -1px;
}
.mainBox[chromedir="rtl"] .submitURLOptIn {
margin-left: 0;
margin-right: -1px;
}
.submitButtonBox {
margin-top: 7px;
}
.submitButton {
float: right;
}
.mainBox[chromedir="rtl"] .submitButton {
float: left;
}
.helpIcon {
float: left;
display: inline-block;
min-width: 16px;
min-height: 16px;
background: url(chrome://mozapps/skin/plugins/pluginHelp-16.png) no-repeat;
cursor: pointer;
float: left;
}
.mainBox[chromedir="rtl"] .helpIcon {
float: right;
}
.closeIcon {

View File

@ -109,17 +109,53 @@ html|a {
min-height: 19px; /* height of biggest line (with throbber) */
}
.msgBottomLinks {
padding-left: 2px;
padding-right: 2px;
.submitComment {
width: 340px;
height: 70px;
padding: 5px;
border: none;
border-radius: 5px;
resize: none;
font-family: inherit;
font-size: inherit;
}
.submitURLOptInBox {
text-align: start;
}
.submitURLOptIn {
margin-left: -1px;
}
.mainBox[chromedir="rtl"] .submitURLOptIn {
margin-left: 0;
margin-right: -1px;
}
.submitButtonBox {
margin-top: 7px;
}
.submitButton {
float: right;
}
.mainBox[chromedir="rtl"] .submitButton {
float: left;
}
.helpIcon {
float: left;
display: inline-block;
min-width: 16px;
min-height: 16px;
background: url(chrome://mozapps/skin/plugins/pluginHelp-16.png) no-repeat;
cursor: pointer;
float: left;
}
.mainBox[chromedir="rtl"] .helpIcon {
float: right;
}
.closeIcon {