Bug 859980 - [netmonitor] JSON request not parsed, r=rcampbell

This commit is contained in:
Victor Porof 2013-04-11 21:11:46 +03:00
parent a476ef0bba
commit 51b054dd0e
8 changed files with 274 additions and 80 deletions

View File

@ -466,6 +466,43 @@ NetworkEventsHandler.prototype = {
eventTimings: aResponse
});
window.emit("NetMonitor:NetworkEventUpdated:EventTimings");
},
/**
* Fetches the full text of a LongString.
*
* @param object | string aStringGrip
* The long string grip containing the corresponding actor.
* If you pass in a plain string (by accident or because you're lazy),
* then a promise of the same string is simply returned.
* @return object Promise
* A promise that is resolved when the full string contents
* are available, or rejected if something goes wrong.
*/
getString: function NEH_getString(aStringGrip) {
// Make sure this is a long string.
if (typeof aStringGrip != "object" || aStringGrip.type != "longString") {
return Promise.resolve(aStringGrip); // Go home string, you're drunk.
}
// Fetch the long string only once.
if (aStringGrip._fullText) {
return aStringGrip._fullText.promise;
}
let deferred = aStringGrip._fullText = Promise.defer();
let { actor, initial, length } = aStringGrip;
let longStringClient = this.webConsoleClient.longString(aStringGrip);
longStringClient.substring(initial.length, length, (aResponse) => {
if (aResponse.error) {
Cu.reportError(aResponse.error + ": " + aResponse.message);
deferred.reject(aResponse);
return;
}
deferred.resolve(initial + aResponse.substring);
});
return deferred.promise;
}
};
@ -497,7 +534,10 @@ NetMonitorController.NetworkEventsHandler = new NetworkEventsHandler();
*/
Object.defineProperties(window, {
"create": {
get: function() ViewHelpers.create,
get: function() ViewHelpers.create
},
"gNetwork": {
get: function() NetMonitorController.NetworkEventsHandler
}
});

View File

@ -886,7 +886,7 @@ create({ constructor: NetworkDetailsView, proto: MenuContainer.prototype }, {
for (let header of aResponse.headers) {
let headerVar = headersScope.addVar(header.name, { null: true }, true);
headerVar.setGrip(header.value);
gNetwork.getString(header.value).then((aString) => headerVar.setGrip(aString));
}
},
@ -929,7 +929,7 @@ create({ constructor: NetworkDetailsView, proto: MenuContainer.prototype }, {
for (let cookie of aResponse.cookies) {
let cookieVar = cookiesScope.addVar(cookie.name, { null: true }, true);
cookieVar.setGrip(cookie.value);
gNetwork.getString(cookie.value).then((aString) => cookieVar.setGrip(aString));
// By default the cookie name and value are shown. If this is the only
// information available, then nothing else is to be displayed.
@ -961,12 +961,7 @@ create({ constructor: NetworkDetailsView, proto: MenuContainer.prototype }, {
let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
let query = uri.query;
if (query) {
this._addParams(this._paramsQueryString, query.split("&").map((e) =>
let (param = e.split("=")) {
name: param[0],
value: NetworkHelper.convertToUnicode(unescape(param[1]))
}
));
this._addParams(this._paramsQueryString, query);
}
},
@ -985,27 +980,28 @@ create({ constructor: NetworkDetailsView, proto: MenuContainer.prototype }, {
let contentType = aHeadersResponse.headers.filter(({ name }) => name == "Content-Type")[0];
let text = aPostResponse.postData.text;
if (contentType.value.contains("x-www-form-urlencoded")) {
this._addParams(this._paramsFormData, text.replace(/^[?&]/, "").split("&").map((e) =>
let (param = e.split("=")) {
name: param[0],
value: NetworkHelper.convertToUnicode(unescape(param[1]))
}
));
} else {
// This is really awkward, but hey, it works. Let's show an empty
// scope in the params view and place the source editor containing
// the raw post data directly underneath.
$("#request-params-box").removeAttribute("flex");
let paramsScope = this._params.addScope(this._paramsPostPayload);
paramsScope.expanded = true;
paramsScope.locked = true;
gNetwork.getString(text).then((aString) => {
// Handle query strings (poor man's forms, e.g. "?foo=bar&baz=42").
if (contentType.value.contains("x-www-form-urlencoded")) {
this._addParams(this._paramsFormData, aString);
}
// Handle actual forms ("multipart/form-data" content type).
else {
// This is really awkward, but hey, it works. Let's show an empty
// scope in the params view and place the source editor containing
// the raw post data directly underneath.
$("#request-params-box").removeAttribute("flex");
let paramsScope = this._params.addScope(this._paramsPostPayload);
paramsScope.expanded = true;
paramsScope.locked = true;
$("#request-post-data-textarea-box").hidden = false;
NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
aEditor.setText(text);
});
}
$("#request-post-data-textarea-box").hidden = false;
NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
aEditor.setText(aString);
});
}
window.emit("NetMonitor:ResponsePostParamsAvailable");
});
},
/**
@ -1013,14 +1009,21 @@ create({ constructor: NetworkDetailsView, proto: MenuContainer.prototype }, {
*
* @param string aName
* The type of params to populate (get or post).
* @param object aParams
* An array containing { name: value } param information tuples.
* @param string aParams
* A query string of params (e.g. "?foo=bar&baz=42").
*/
_addParams: function NVND__addParams(aName, aParams) {
// Turn the params string into an array containing { name: value } tuples.
let paramsArray = aParams.replace(/^[?&]/, "").split("&").map((e) =>
let (param = e.split("=")) {
name: NetworkHelper.convertToUnicode(unescape(param[0])),
value: NetworkHelper.convertToUnicode(unescape(param[1]))
});
let paramsScope = this._params.addScope(aName);
paramsScope.expanded = true;
for (let param of aParams) {
for (let param of paramsArray) {
let headerVar = paramsScope.addVar(param.name, { null: true }, true);
headerVar.setGrip(param.value);
}
@ -1039,52 +1042,59 @@ create({ constructor: NetworkDetailsView, proto: MenuContainer.prototype }, {
return;
}
let uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
let { mimeType: mime, text, encoding } = aResponse.content;
let { mimeType, text, encoding } = aResponse.content;
if (mime.contains("/json")) {
$("#response-content-json-box").hidden = false;
let jsonScope = this._json.addScope("JSON");
let sanitizedText = text.replace(/^[a-zA-Z0-9_$]+\(|\)$/g, ""); // JSONP
jsonScope.addVar().populate(JSON.parse(sanitizedText), { expanded: true });
jsonScope.expanded = true;
}
else if (mime.contains("image/")) {
$("#response-content-image-box").setAttribute("align", "center");
$("#response-content-image-box").setAttribute("pack", "center");
$("#response-content-image-box").hidden = false;
$("#response-content-image").src = "data:" + mime + ";" + encoding + "," + text;
gNetwork.getString(text).then((aString) => {
// Handle json.
if (mimeType.contains("/json")) {
$("#response-content-json-box").hidden = false;
let jsonScope = this._json.addScope("JSON");
let sanitizedText = aString.replace(/^[a-zA-Z0-9_$]+\(|\)$/g, ""); // JSONP
jsonScope.addVar().populate(JSON.parse(sanitizedText), { expanded: true });
jsonScope.expanded = true;
}
// Handle images.
else if (mimeType.contains("image/")) {
$("#response-content-image-box").setAttribute("align", "center");
$("#response-content-image-box").setAttribute("pack", "center");
$("#response-content-image-box").hidden = false;
$("#response-content-image").src =
"data:" + mimeType + ";" + encoding + "," + aString;
// Immediately display additional information about the image:
// file name, mime type and encoding.
$("#response-content-image-name-value").setAttribute("value", uri.fileName);
$("#response-content-image-mime-value").setAttribute("value", mime);
$("#response-content-image-encoding-value").setAttribute("value", encoding);
// Immediately display additional information about the image:
// file name, mime type and encoding.
$("#response-content-image-name-value").setAttribute("value", uri.fileName);
$("#response-content-image-mime-value").setAttribute("value", mimeType);
$("#response-content-image-encoding-value").setAttribute("value", encoding);
// Wait for the image to load in order to display the width and height.
$("#response-content-image").onload = (e) => {
// XUL images are majestic so they don't bother storing their dimensions
// in width and height attributes like the rest of the folk. Hack around
// this by getting the bounding client rect and subtracting the margins.
let { width, height } = e.target.getBoundingClientRect();
let dimensions = (width - 2) + " x " + (height - 2);
$("#response-content-image-dimensions-value").setAttribute("value", dimensions);
};
}
else {
$("#response-content-textarea-box").hidden = false;
NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
aEditor.setMode(SourceEditor.MODES.TEXT);
aEditor.setText(typeof text == "string" ? text : text.initial);
// Wait for the image to load in order to display the width and height.
$("#response-content-image").onload = (e) => {
// XUL images are majestic so they don't bother storing their dimensions
// in width and height attributes like the rest of the folk. Hack around
// this by getting the bounding client rect and subtracting the margins.
let { width, height } = e.target.getBoundingClientRect();
let dimensions = (width - 2) + " x " + (height - 2);
$("#response-content-image-dimensions-value").setAttribute("value", dimensions);
};
}
// Handle anything else.
else {
$("#response-content-textarea-box").hidden = false;
NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
aEditor.setMode(SourceEditor.MODES.TEXT);
aEditor.setText(aString);
// Maybe set a more appropriate mode in the Source Editor if possible.
for (let key in CONTENT_MIME_TYPE_MAPPINGS) {
if (mime.contains(key)) {
aEditor.setMode(CONTENT_MIME_TYPE_MAPPINGS[key]);
break;
// Maybe set a more appropriate mode in the Source Editor if possible.
for (let key in CONTENT_MIME_TYPE_MAPPINGS) {
if (mimeType.contains(key)) {
aEditor.setMode(CONTENT_MIME_TYPE_MAPPINGS[key]);
break;
}
}
}
});
}
});
}
window.emit("NetMonitor:ResponseBodyAvailable");
});
},
/**

View File

@ -24,6 +24,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_net_status-codes.js \
browser_net_post-data.js \
browser_net_jsonp.js \
browser_net_json-long.js \
head.js \
$(NULL)
@ -35,6 +36,7 @@ MOCHITEST_BROWSER_PAGES = \
html_status-codes-test-page.html \
html_post-data-test-page.html \
html_jsonp-test-page.html \
html_json-long-test-page.html \
sjs_simple-test-server.sjs \
sjs_content-type-test-server.sjs \
sjs_status-codes-test-server.sjs \

View File

@ -0,0 +1,94 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if JSONP responses are handled correctly.
*/
function test() {
initNetMonitor(JSON_LONG_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
// This is receiving over 80 KB of json and will populate over 6000 items
// in a variables view instance. Debug builds are slow.
requestLongerTimeout(2);
let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
let { RequestsMenu } = NetMonitorView;
RequestsMenu.lazyUpdate = false;
waitForNetworkEvents(aMonitor, 1).then(() => {
verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
"GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
status: 200,
statusText: "OK",
type: "json",
fullMimeType: "text/json; charset=utf-8",
size: L10N.getFormatStr("networkMenu.sizeKB", 83.96),
time: true
});
EventUtils.sendMouseEvent({ type: "mousedown" },
document.getElementById("details-pane-toggle"));
EventUtils.sendMouseEvent({ type: "mousedown" },
document.querySelectorAll("#details-pane tab")[3]);
aMonitor.panelWin.once("NetMonitor:ResponseBodyAvailable", () => {
testResponseTab();
teardown(aMonitor).then(finish);
});
function testResponseTab() {
let tab = document.querySelectorAll("#details-pane tab")[3];
let tabpanel = document.querySelectorAll("#details-pane tabpanel")[3];
is(tab.getAttribute("selected"), "true",
"The response tab in the network details pane should be selected.");
is(tabpanel.querySelector("#response-content-json-box")
.hasAttribute("hidden"), false,
"The response content json box doesn't have the intended visibility.");
is(tabpanel.querySelector("#response-content-textarea-box")
.hasAttribute("hidden"), true,
"The response content textarea box doesn't have the intended visibility.");
is(tabpanel.querySelector("#response-content-image-box")
.hasAttribute("hidden"), true,
"The response content image box doesn't have the intended visibility.");
is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
"There should be 1 json scope displayed in this tabpanel.");
is(tabpanel.querySelectorAll(".variables-view-property").length, 6057,
"There should be 6057 json properties displayed in this tabpanel.");
is(tabpanel.querySelectorAll(".variables-view-empty-notice").length, 0,
"The empty notice should not be displayed in this tabpanel.");
let jsonScope = tabpanel.querySelectorAll(".variables-view-scope")[0];
let names = ".variables-view-property .name";
let values = ".variables-view-property .value";
is(jsonScope.querySelector(".name").getAttribute("value"),
"JSON", "The json scope doesn't have the correct title.");
is(jsonScope.querySelectorAll(names)[0].getAttribute("value"),
"0", "The first json property name was incorrect.");
is(jsonScope.querySelectorAll(values)[0].getAttribute("value"),
"[object Object]", "The first json property value was incorrect.");
is(jsonScope.querySelectorAll(names)[1].getAttribute("value"),
"greeting", "The second json property name was incorrect.");
is(jsonScope.querySelectorAll(values)[1].getAttribute("value"),
"\"Hello long string JSON!\"", "The second json property value was incorrect.");
is(Array.slice(jsonScope.querySelectorAll(names), -1).shift().getAttribute("value"),
"__proto__", "The last json property name was incorrect.");
is(Array.slice(jsonScope.querySelectorAll(values), -1).shift().getAttribute("value"),
"[object Object]", "The last json property value was incorrect.");
}
});
aDebuggee.performRequests();
});
}

View File

@ -30,7 +30,7 @@ function test() {
EventUtils.sendMouseEvent({ type: "mousedown" },
document.querySelectorAll("#details-pane tab")[3]);
testResponseTab("jsonp");
testResponseTab();
teardown(aMonitor).then(finish);
function testResponseTab() {

View File

@ -17,6 +17,7 @@ const CONTENT_TYPE_URL = EXAMPLE_URL + "html_content-type-test-page.html";
const STATUS_CODES_URL = EXAMPLE_URL + "html_status-codes-test-page.html";
const POST_DATA_URL = EXAMPLE_URL + "html_post-data-test-page.html";
const JSONP_URL = EXAMPLE_URL + "html_jsonp-test-page.html";
const JSON_LONG_URL = EXAMPLE_URL + "html_json-long-test-page.html";
const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";

View File

@ -0,0 +1,33 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Network Monitor test page</title>
</head>
<body>
<p>JSON long string test</p>
<script type="text/javascript">
function get(aAddress, aCallback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", aAddress, true);
xhr.onreadystatechange = function() {
if (this.readyState == this.DONE) {
aCallback();
}
};
xhr.send(null);
}
function performRequests() {
get("sjs_content-type-test-server.sjs?fmt=json-long", function() {
// Done.
});
}
</script>
</body>
</html>

View File

@ -11,43 +11,57 @@ function handleRequest(request, response) {
Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer).initWithCallback(() => {
switch (format) {
case "xml":
case "xml": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "text/xml; charset=utf-8", false);
response.write("<label value='greeting'>Hello XML!</label>");
response.finish();
break;
case "css":
}
case "css": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "text/css; charset=utf-8", false);
response.write("body:pre { content: 'Hello CSS!' }");
response.finish();
break;
case "js":
}
case "js": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/javascript; charset=utf-8", false);
response.write("function() { return 'Hello JS!'; }");
response.finish();
break;
case "json":
}
case "json": {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/json; charset=utf-8", false);
response.write("{ \"greeting\": \"Hello JSON!\" }");
response.finish();
break;
case "jsonp":
}
case "jsonp": {
let fun = params.filter((s) => s.contains("jsonp="))[0].split("=")[1];
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "text/json; charset=utf-8", false);
response.write(fun + "({ \"greeting\": \"Hello JSONP!\" })");
response.finish();
break;
default:
}
case "json-long": {
let str = "{ \"greeting\": \"Hello long string JSON!\" },";
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "text/json; charset=utf-8", false);
response.write("[" + new Array(2048).join(str).slice(0, -1) + "]");
response.finish();
break;
}
default: {
response.setStatusLine(request.httpVersion, 404, "Not Found");
response.setHeader("Content-Type", "text/html; charset=utf-8", false);
response.write("<blink>Not Found</blink>");
response.finish();
break;
}
}
}, 10, Ci.nsITimer.TYPE_ONE_SHOT); // Make sure this request takes a few ms.
}