Merge f-t to m-c

This commit is contained in:
Phil Ringnalda 2014-03-22 17:40:04 -07:00
commit a6e09c5c74
20 changed files with 1267 additions and 1 deletions

View File

@ -41,9 +41,11 @@ ifndef MOZ_PROFILE_USE
# otherwise the rule in rules.mk doesn't run early enough.
libs binaries export tools:: CLOBBER $(configure_dir)/configure config.status backend.RecursiveMakeBackend
ifndef JS_STANDALONE
ifndef LIBXUL_SDK
libs binaries export tools:: $(topsrcdir)/js/src/configure js/src/config.status
endif
endif
endif
ifdef JS_STANDALONE
.PHONY: CLOBBER
@ -101,11 +103,13 @@ install_manifest_depends = \
$(NULL)
ifndef JS_STANDALONE
ifndef LIBXUL_SDK
install_manifest_depends += \
$(topsrcdir)/js/src/configure \
js/src/config.status \
$(NULL)
endif
endif
.PHONY: install-manifests
install-manifests: $(addprefix install-dist-,$(install_manifests))

View File

@ -140,6 +140,9 @@ Object.defineProperty(this, "NetworkHelper", {
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
XPCOMUtils.defineLazyModuleGetter(this, "Curl",
"resource:///modules/devtools/Curl.jsm");
/**
* Object defining the network monitor controller components.
*/

View File

@ -523,6 +523,37 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
clipboardHelper.copyString(selected.url, document);
},
/**
* Copy a cURL command from the currently selected item.
*/
copyAsCurl: function() {
let selected = this.selectedItem.attachment;
Task.spawn(function*() {
// Create a sanitized object for the Curl command generator.
let data = {
url: selected.url,
method: selected.method,
headers: [],
httpVersion: selected.httpVersion,
postDataText: null
};
// Fetch header values.
for (let { name, value } of selected.requestHeaders.headers) {
let text = yield gNetwork.getString(value);
data.headers.push({ name: name, value: text });
}
// Fetch the request payload.
if (selected.requestPostData) {
let postData = selected.requestPostData.postData.text;
data.postDataText = yield gNetwork.getString(postData);
}
clipboardHelper.copyString(Curl.generateCommand(data), document);
});
},
/**
* Copy image as data uri.
*/
@ -1560,6 +1591,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
let copyUrlElement = $("#request-menu-context-copy-url");
copyUrlElement.hidden = !selectedItem;
let copyAsCurlElement = $("#request-menu-context-copy-as-curl");
copyAsCurlElement.hidden = !selectedItem || !selectedItem.attachment.responseContent;
let copyImageAsDataUriElement = $("#request-menu-context-copy-image-as-data-uri");
copyImageAsDataUriElement.hidden = !selectedItem ||
!selectedItem.attachment.responseContent ||

View File

@ -29,6 +29,9 @@
<menuitem id="request-menu-context-copy-url"
label="&netmonitorUI.context.copyUrl;"
accesskey="&netmonitorUI.context.copyUrl.accesskey;"/>
<menuitem id="request-menu-context-copy-as-curl"
label="&netmonitorUI.context.copyAsCurl;"
oncommand="NetMonitorView.RequestsMenu.copyAsCurl();"/>
<menuitem id="request-menu-context-copy-image-as-data-uri"
label="&netmonitorUI.context.copyImageAsDataUri;"
accesskey="&netmonitorUI.context.copyImageAsDataUri.accesskey;"/>

View File

@ -21,6 +21,8 @@ support-files =
html_sorting-test-page.html
html_statistics-test-page.html
html_status-codes-test-page.html
html_copy-as-curl.html
html_curl-utils.html
sjs_content-type-test-server.sjs
sjs_simple-test-server.sjs
sjs_sorting-test-server.sjs
@ -39,8 +41,10 @@ support-files =
[browser_net_clear.js]
[browser_net_complex-params.js]
[browser_net_content-type.js]
[browser_net_curl-utils.js]
[browser_net_copy_image_as_data_uri.js]
[browser_net_copy_url.js]
[browser_net_copy_as_curl.js]
[browser_net_cyrillic-01.js]
[browser_net_cyrillic-02.js]
[browser_net_filter-01.js]

View File

@ -0,0 +1,72 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if Copy as cURL works.
*/
function test() {
initNetMonitor(CURL_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
const EXPECTED_POSIX_RESULT = [
"curl",
"'" + SIMPLE_SJS + "'",
"-H 'Host: example.com'",
"-H 'User-Agent: " + aDebuggee.navigator.userAgent + "'",
"-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'",
"-H 'Accept-Language: " + aDebuggee.navigator.language + "'",
"-H 'Accept-Encoding: gzip, deflate'",
"-H 'X-Custom-Header-1: Custom value'",
"-H 'X-Custom-Header-2: 8.8.8.8'",
"-H 'X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT'",
"-H 'Referer: " + CURL_URL + "'",
"-H 'Connection: keep-alive'"
].join(" ");
const EXPECTED_WIN_RESULT = [
'curl',
'"' + SIMPLE_SJS + '"',
'-H "Host: example.com"',
'-H "User-Agent: ' + aDebuggee.navigator.userAgent + '"',
'-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"',
'-H "Accept-Language: ' + aDebuggee.navigator.language + '"',
'-H "Accept-Encoding: gzip, deflate"',
'-H "X-Custom-Header-1: Custom value"',
'-H "X-Custom-Header-2: 8.8.8.8"',
'-H "X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"',
'-H "Referer: ' + CURL_URL + '"',
'-H "Connection: keep-alive"'
].join(" ");
const EXPECTED_RESULT = Services.appinfo.OS == "WINNT" ?
EXPECTED_WIN_RESULT : EXPECTED_POSIX_RESULT;
let { NetMonitorView } = aMonitor.panelWin;
let { RequestsMenu } = NetMonitorView;
RequestsMenu.lazyUpdate = false;
waitForNetworkEvents(aMonitor, 1).then(() => {
let requestItem = RequestsMenu.getItemAtIndex(0);
RequestsMenu.selectedItem = requestItem;
waitForClipboard(EXPECTED_RESULT, function setup() {
RequestsMenu.copyAsCurl();
}, function onSuccess() {
ok(true, "Clipboard contains a cURL command for the currently selected item's url.");
cleanUp();
}, function onFailure() {
ok(false, "Creating a cURL command for the currently selected item was unsuccessful.");
cleanUp();
});
});
aDebuggee.performRequest(SIMPLE_SJS);
function cleanUp(){
teardown(aMonitor).then(finish);
}
});
}

View File

@ -0,0 +1,232 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests Curl Utils functionality.
*/
function test() {
initNetMonitor(CURL_UTILS_URL).then(([aTab, aDebuggee, aMonitor]) => {
info("Starting test... ");
let { NetMonitorView, gNetwork } = aMonitor.panelWin;
let { RequestsMenu } = NetMonitorView;
RequestsMenu.lazyUpdate = false;
waitForNetworkEvents(aMonitor, 1, 3).then(() => {
let requests = {
get: RequestsMenu.getItemAtIndex(0),
post: RequestsMenu.getItemAtIndex(1),
multipart: RequestsMenu.getItemAtIndex(2),
multipartForm: RequestsMenu.getItemAtIndex(3)
};
Task.spawn(function*() {
yield createCurlData(requests.get.attachment, gNetwork).then((aData) => {
test_findHeader(aData);
});
yield createCurlData(requests.post.attachment, gNetwork).then((aData) => {
test_isUrlEncodedRequest(aData);
test_writePostDataTextParams(aData);
});
yield createCurlData(requests.multipart.attachment, gNetwork).then((aData) => {
test_isMultipartRequest(aData);
test_getMultipartBoundary(aData);
test_removeBinaryDataFromMultipartText(aData);
});
yield createCurlData(requests.multipartForm.attachment, gNetwork).then((aData) => {
test_getHeadersFromMultipartText(aData);
});
if (Services.appinfo.OS != "WINNT") {
test_escapeStringPosix();
} else {
test_escapeStringWin();
}
teardown(aMonitor).then(finish);
});
});
aDebuggee.performRequests(SIMPLE_SJS);
});
}
function test_isUrlEncodedRequest(aData) {
let isUrlEncoded = CurlUtils.isUrlEncodedRequest(aData);
ok(isUrlEncoded, "Should return true for url encoded requests.");
}
function test_isMultipartRequest(aData) {
let isMultipart = CurlUtils.isMultipartRequest(aData);
ok(isMultipart, "Should return true for multipart/form-data requests.");
}
function test_findHeader(aData) {
let headers = aData.headers;
let hostName = CurlUtils.findHeader(headers, "Host");
let requestedWithLowerCased = CurlUtils.findHeader(headers, "x-requested-with");
let doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist");
is(hostName, "example.com",
"Header with name 'Host' should be found in the request array.");
is(requestedWithLowerCased, "XMLHttpRequest",
"The search should be case insensitive.");
is(doesNotExist, null,
"Should return null when a header is not found.");
}
function test_writePostDataTextParams(aData) {
let params = CurlUtils.writePostDataTextParams(aData.postDataText);
is(params, "param1=value1&param2=value2&param3=value3",
"Should return a serialized representation of the request parameters");
}
function test_getMultipartBoundary(aData) {
let boundary = CurlUtils.getMultipartBoundary(aData);
ok(/-{3,}\w+/.test(boundary),
"A boundary string should be found in a multipart request.");
}
function test_removeBinaryDataFromMultipartText(aData) {
let generatedBoundary = CurlUtils.getMultipartBoundary(aData);
let text = aData.postDataText;
let binaryRemoved =
CurlUtils.removeBinaryDataFromMultipartText(text, generatedBoundary);
let boundary = "--" + generatedBoundary;
const EXPECTED_POSIX_RESULT = [
"$'",
boundary,
"\\r\\n\\r\\n",
"Content-Disposition: form-data; name=\"param1\"",
"\\r\\n\\r\\n",
"value1",
"\\r\\n",
boundary,
"\\r\\n\\r\\n",
"Content-Disposition: form-data; name=\"file\"; filename=\"filename.png\"",
"\\r\\n",
"Content-Type: image/png",
"\\r\\n\\r\\n",
generatedBoundary,
"--\\r\\n",
"'"
].join("");
const EXPECTED_WIN_RESULT = [
'"' + boundary + '"^',
'\u000d\u000A\u000d\u000A',
'"Content-Disposition: form-data; name=""param1"""^',
'\u000d\u000A\u000d\u000A',
'"value1"^',
'\u000d\u000A',
'"' + boundary + '"^',
'\u000d\u000A\u000d\u000A',
'"Content-Disposition: form-data; name=""file""; filename=""filename.png"""^',
'\u000d\u000A',
'"Content-Type: image/png"^',
'\u000d\u000A\u000d\u000A',
'"' + generatedBoundary + '--"^',
'\u000d\u000A',
'""'
].join("");
if (Services.appinfo.OS != "WINNT") {
is(CurlUtils.escapeStringPosix(binaryRemoved), EXPECTED_POSIX_RESULT,
"The mulitpart request payload should not contain binary data.");
} else {
is(CurlUtils.escapeStringWin(binaryRemoved), EXPECTED_WIN_RESULT,
"WinNT: The mulitpart request payload should not contain binary data.");
}
}
function test_getHeadersFromMultipartText(aData) {
let headers = CurlUtils.getHeadersFromMultipartText(aData.postDataText);
ok(Array.isArray(headers),
"Should return an array.");
ok(headers.length > 0,
"There should exist at least one request header.");
is(headers[0].name, "Content-Type",
"The first header name should be 'Content-Type'.");
}
function test_escapeStringPosix() {
let surroundedWithQuotes = "A simple string";
is(CurlUtils.escapeStringPosix(surroundedWithQuotes), "'A simple string'",
"The string should be surrounded with single quotes.");
let singleQuotes = "It's unusual to put crickets in your coffee.";
is(CurlUtils.escapeStringPosix(singleQuotes),
"$'It\\'s unusual to put crickets in your coffee.'",
"Single quotes should be escaped.");
let newLines = "Line 1\r\nLine 2\u000d\u000ALine3";
is(CurlUtils.escapeStringPosix(newLines), "$'Line 1\\r\\nLine 2\\r\\nLine3'",
"Newlines should be escaped.");
let controlChars = "\u0007 \u0009 \u000C \u001B";
is(CurlUtils.escapeStringPosix(controlChars), "$'\\x07 \\x09 \\x0c \\x1b'",
"Control characters should be escaped.");
let extendedAsciiChars = "æ ø ü ß ö é";
is(CurlUtils.escapeStringPosix(extendedAsciiChars),
"$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'",
"Character codes outside of the decimal range 32 - 126 should be escaped.");
}
function test_escapeStringWin() {
let surroundedWithDoubleQuotes = "A simple string";
is(CurlUtils.escapeStringWin(surroundedWithDoubleQuotes), '"A simple string"',
"The string should be surrounded with double quotes.");
let doubleQuotes = "Quote: \"Time is an illusion. Lunchtime doubly so.\"";
is(CurlUtils.escapeStringWin(doubleQuotes),
'"Quote: ""Time is an illusion. Lunchtime doubly so."""',
"Double quotes should be escaped.");
let percentSigns = "%AppData%";
is(CurlUtils.escapeStringWin(percentSigns), '""%"AppData"%""',
"Percent signs should be escaped.");
let backslashes = "\\A simple string\\";
is(CurlUtils.escapeStringWin(backslashes), '"\\\\A simple string\\\\"',
"Backslashes should be escaped.");
let newLines = "line1\r\nline2\r\nline3";
is(CurlUtils.escapeStringWin(newLines),
'"line1"^\u000d\u000A"line2"^\u000d\u000A"line3"',
"Newlines should be escaped.");
}
function createCurlData(aSelected, aNetwork) {
return Task.spawn(function*() {
// Create a sanitized object for the Curl command generator.
let data = {
url: aSelected.url,
method: aSelected.method,
headers: [],
httpVersion: aSelected.httpVersion,
postDataText: null
};
// Fetch header values.
for (let { name, value } of aSelected.requestHeaders.headers) {
let text = yield aNetwork.getString(value);
data.headers.push({ name: name, value: text });
}
// Fetch the request payload.
if (aSelected.requestPostData) {
let postData = aSelected.requestPostData.postData.text;
data.postDataText = yield aNetwork.getString(postData);
}
return data;
});
}

View File

@ -9,6 +9,7 @@ let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let { CurlUtils } = Cu.import("resource:///modules/devtools/Curl.jsm", {});
let TargetFactory = devtools.TargetFactory;
let Toolbox = devtools.Toolbox;
@ -34,6 +35,8 @@ const FILTERING_URL = EXAMPLE_URL + "html_filter-test-page.html";
const INFINITE_GET_URL = EXAMPLE_URL + "html_infinite-get-page.html";
const CUSTOM_GET_URL = EXAMPLE_URL + "html_custom-get-page.html";
const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
const CURL_URL = EXAMPLE_URL + "html_copy-as-curl.html";
const CURL_UTILS_URL = EXAMPLE_URL + "html_curl-utils.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,27 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Network Monitor test page</title>
</head>
<body>
<p>Performing a GET request</p>
<script type="text/javascript">
function performRequest(aUrl) {
var xhr = new XMLHttpRequest();
xhr.open("GET", aUrl, true);
xhr.setRequestHeader("Accept-Language", window.navigator.language);
xhr.setRequestHeader("X-Custom-Header-1", "Custom value");
xhr.setRequestHeader("X-Custom-Header-2", "8.8.8.8");
xhr.setRequestHeader("X-Custom-Header-3", "Mon, 3 Mar 2014 11:11:11 GMT");
xhr.send(null);
}
</script>
</body>
</html>

View File

@ -0,0 +1,99 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Network Monitor test page</title>
</head>
<body>
<p>Performing requests</p>
<p>
<canvas width="100" height="100"></canvas>
</p>
<hr/>
<form method="post" action="#" enctype="multipart/form-data" target="target" id="post-form">
<input type="text" name="param1" value="value1"/>
<input type="text" name="param2" value="value2"/>
<input type="text" name="param3" value="value3"/>
<input type="submit"/>
</form>
<iframe name="target"></iframe>
<script type="text/javascript">
function ajaxGet(aUrl, aCallback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", aUrl + "?param1=value1&param2=value2&param3=value3", true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.onload = function() {
aCallback();
};
xhr.send();
}
function ajaxPost(aUrl, aCallback) {
var xhr = new XMLHttpRequest();
xhr.open("POST", aUrl, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.onload = function() {
aCallback();
};
var params = "param1=value1&param2=value2&param3=value3";
xhr.send(params);
}
function ajaxMultipart(aUrl, aCallback) {
var xhr = new XMLHttpRequest();
xhr.open("POST", aUrl, true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.onload = function() {
aCallback();
};
getCanvasElem().toBlob((blob) => {
var formData = new FormData();
formData.append("param1", "value1");
formData.append("file", blob, "filename.png");
xhr.send(formData);
});
}
function submitForm() {
var form = document.querySelector("#post-form");
form.submit();
}
function getCanvasElem() {
return document.querySelector("canvas");
}
function initCanvas() {
var canvas = getCanvasElem();
var ctx = canvas.getContext("2d");
ctx.fillRect(0,0,100,100);
ctx.clearRect(20,20,60,60);
ctx.strokeRect(25,25,50,50);
}
function performRequests(aUrl) {
ajaxGet(aUrl, () => {
ajaxPost(aUrl, () => {
ajaxMultipart(aUrl, () => {
submitForm();
});
});
});
}
initCanvas();
</script>
</body>
</html>

View File

@ -0,0 +1,396 @@
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 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/. */
/*
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
* Copyright (C) 2011 Google Inc. All rights reserved.
* Copyright (C) 2009 Mozilla Foundation. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
"use strict";
this.EXPORTED_SYMBOLS = ["Curl", "CurlUtils"];
Components.utils.import("resource://gre/modules/Services.jsm");
const DEFAULT_HTTP_VERSION = "HTTP/1.1";
this.Curl = {
/**
* Generates a cURL command string which can be used from the command line etc.
*
* @param object aData
* Datasource to create the command from.
* The object must contain the following properties:
* - url:string, the URL of the request.
* - method:string, the request method upper cased. HEAD / GET / POST etc.
* - headers:array, an array of request headers {name:x, value:x} tuples.
* - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1"
* - postDataText:string, optional - the request payload.
*
* @return string
* A cURL command.
*/
generateCommand: function(aData) {
let utils = CurlUtils;
let command = ["curl"];
let ignoredHeaders = new Set();
// The cURL command is expected to run on the same platform that Firefox runs
// (it may be different from the inspected page platform).
let escapeString = Services.appinfo.OS == "WINNT" ?
utils.escapeStringWin : utils.escapeStringPosix;
// Add URL.
command.push(escapeString(aData.url));
let postDataText = null;
let multipartRequest = utils.isMultipartRequest(aData);
// Create post data.
let data = [];
if (utils.isUrlEncodedRequest(aData) || aData.method == "PUT") {
postDataText = aData.postDataText;
data.push("--data");
data.push(escapeString(utils.writePostDataTextParams(postDataText)));
ignoredHeaders.add("Content-Length");
} else if (multipartRequest) {
postDataText = aData.postDataText;
data.push("--data-binary");
let boundary = utils.getMultipartBoundary(aData);
let text = utils.removeBinaryDataFromMultipartText(postDataText, boundary);
data.push(escapeString(text));
ignoredHeaders.add("Content-Length");
}
// Add method.
// For GET and POST requests this is not necessary as GET is the
// default. If --data or --binary is added POST is the default.
if (!(aData.method == "GET" || aData.method == "POST")) {
command.push("-X");
command.push(aData.method);
}
// Add -I (HEAD)
// For servers that supports HEAD.
// This will fetch the header of a document only.
if (aData.method == "HEAD") {
command.push("-I");
}
// Add http version.
if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) {
command.push("--" + aData.httpVersion.split("/")[1]);
}
// Add request headers.
let headers = aData.headers;
if (multipartRequest) {
let multipartHeaders = utils.getHeadersFromMultipartText(postDataText);
headers = headers.concat(multipartHeaders);
}
for (let i = 0; i < headers.length; i++) {
let header = headers[i];
if (ignoredHeaders.has(header.name)) {
continue;
}
command.push("-H");
command.push(escapeString(header.name + ": " + header.value));
}
// Add post data.
command = command.concat(data);
return command.join(" ");
}
};
/**
* Utility functions for the Curl command generator.
*/
this.CurlUtils = {
/**
* Check if the request is an URL encoded request.
*
* @param object aData
* The data source. See the description in the Curl object.
* @return boolean
* True if the request is URL encoded, false otherwise.
*/
isUrlEncodedRequest: function(aData) {
let postDataText = aData.postDataText;
if (!postDataText) {
return false;
}
postDataText = postDataText.toLowerCase();
if (postDataText.contains("content-type: application/x-www-form-urlencoded")) {
return true;
}
let contentType = this.findHeader(aData.headers, "content-type");
return (contentType &&
contentType.toLowerCase().contains("application/x-www-form-urlencoded"));
},
/**
* Check if the request is a multipart request.
*
* @param object aData
* The data source.
* @return boolean
* True if the request is multipart reqeust, false otherwise.
*/
isMultipartRequest: function(aData) {
let postDataText = aData.postDataText;
if (!postDataText) {
return false;
}
postDataText = postDataText.toLowerCase();
if (postDataText.contains("content-type: multipart/form-data")) {
return true;
}
let contentType = this.findHeader(aData.headers, "content-type");
return (contentType &&
contentType.toLowerCase().contains("multipart/form-data;"));
},
/**
* Write out paramters from post data text.
*
* @param object aPostDataText
* Post data text.
* @return string
* Post data parameters.
*/
writePostDataTextParams: function(aPostDataText) {
let lines = aPostDataText.split("\r\n");
return lines[lines.length - 1];
},
/**
* Finds the header with the given name in the headers array.
*
* @param array aHeaders
* Array of headers info {name:x, value:x}.
* @param string aName
* The header name to find.
* @return string
* The found header value or null if not found.
*/
findHeader: function(aHeaders, aName) {
if (!aHeaders) {
return null;
}
let name = aName.toLowerCase();
for (let header of aHeaders) {
if (name == header.name.toLowerCase()) {
return header.value;
}
}
return null;
},
/**
* Returns the boundary string for a multipart request.
*
* @param string aData
* The data source. See the description in the Curl object.
* @return string
* The boundary string for the request.
*/
getMultipartBoundary: function(aData) {
let boundaryRe = /\bboundary=(-{3,}\w+)/i;
// Get the boundary string from the Content-Type request header.
let contentType = this.findHeader(aData.headers, "Content-Type");
if (boundaryRe.test(contentType)) {
return contentType.match(boundaryRe)[1];
}
// Temporary workaround. As of 2014-03-11 the requestHeaders array does not
// always contain the Content-Type header for mulitpart requests. See bug 978144.
// Find the header from the request payload.
let boundaryString = aData.postDataText.match(boundaryRe)[1];
if (boundaryString) {
return boundaryString;
}
return null;
},
/**
* Removes the binary data from mulitpart text.
*
* @param string aMultipartText
* Multipart form data text.
* @param string aBoundary
* The boundary string.
* @return string
* The mulitpart text without the binary data.
*/
removeBinaryDataFromMultipartText: function(aMultipartText, aBoundary) {
let result = "";
let boundary = "--" + aBoundary;
let parts = aMultipartText.split(boundary);
for (let part of parts) {
// Each part is expected to have a content disposition line.
let contentDispositionLine = part.trimLeft().split("\r\n")[0];
if (!contentDispositionLine) {
continue;
}
contentDispositionLine = contentDispositionLine.toLowerCase();
if (contentDispositionLine.contains("content-disposition: form-data")) {
if (contentDispositionLine.contains("filename=")) {
// The header lines and the binary blob is separated by 2 CRLF's.
// Add only the headers to the result.
let headers = part.split("\r\n\r\n")[0];
result += boundary + "\r\n" + headers + "\r\n\r\n";
}
else {
result += boundary + "\r\n" + part;
}
}
}
result += aBoundary + "--\r\n";
return result;
},
/**
* Get the headers from a multipart post data text.
*
* @param string aMultipartText
* Multipart post text.
* @return array
* An array of header objects {name:x, value:x}
*/
getHeadersFromMultipartText: function(aMultipartText) {
let headers = [];
if (!aMultipartText || aMultipartText.startsWith("---")) {
return headers;
}
// Get the header section.
let index = aMultipartText.indexOf("\r\n\r\n");
if (index == -1) {
return headers;
}
// Parse the header lines.
let headersText = aMultipartText.substring(0, index);
let headerLines = headersText.split("\r\n");
let lastHeaderName = null;
for (let line of headerLines) {
// Create a header for each line in fields that spans across multiple lines.
// Subsquent lines always begins with at least one space or tab character.
// (rfc2616)
if (lastHeaderName && /^\s+/.test(line)) {
headers.push({ name: lastHeaderName, value: line.trim() });
continue;
}
let indexOfColon = line.indexOf(":");
if (indexOfColon == -1) {
continue;
}
let header = [line.slice(0, indexOfColon), line.slice(indexOfColon + 1)];
if (header.length != 2) {
continue;
}
lastHeaderName = header[0].trim();
headers.push({ name: lastHeaderName, value: header[1].trim() });
}
return headers;
},
/**
* Escape util function for POSIX oriented operating systems.
* Credit: Google DevTools
*/
escapeStringPosix: function(str) {
function escapeCharacter(x) {
let code = x.charCodeAt(0);
if (code < 256) {
// Add leading zero when needed to not care about the next character.
return code < 16 ? "\\x0" + code.toString(16) : "\\x" + code.toString(16);
}
code = code.toString(16);
return "\\u" + ("0000" + code).substr(code.length, 4);
}
if (/[^\x20-\x7E]|\'/.test(str)) {
// Use ANSI-C quoting syntax.
return "$\'" + str.replace(/\\/g, "\\\\")
.replace(/\'/g, "\\\'")
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
} else {
// Use single quote syntax.
return "'" + str + "'";
}
},
/**
* Escape util function for Windows systems.
* Credit: Google DevTools
*/
escapeStringWin: function(str) {
/* Replace quote by double quote (but not by \") because it is
recognized by both cmd.exe and MS Crt arguments parser.
Replace % by "%" because it could be expanded to an environment
variable value. So %% becomes "%""%". Even if an env variable ""
(2 doublequotes) is declared, the cmd.exe will not
substitute it with its value.
Replace each backslash with double backslash to make sure
MS Crt arguments parser won't collapse them.
Replace new line outside of quotes since cmd.exe doesn't let
to do it inside.
*/
return "\"" + str.replace(/"/g, "\"\"")
.replace(/%/g, "\"%\"")
.replace(/\\/g, "\\\\")
.replace(/[\r\n]+/g, "\"^$&\"") + "\"";
}
};

View File

@ -6,6 +6,7 @@
this.EXPORTED_SYMBOLS = [
"Experiments",
"ExperimentsProvider",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
@ -19,6 +20,7 @@ Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
"resource://gre/modules/UpdateChannel.jsm");
@ -388,6 +390,45 @@ Experiments.Experiments.prototype = {
);
},
/**
* Determine whether another date has the same UTC day as now().
*/
_dateIsTodayUTC: function (d) {
let now = this._policy.now();
return stripDateToMidnight(now).getTime() == stripDateToMidnight(d).getTime();
},
/**
* Obtain the entry of the most recent active experiment that was active
* today.
*
* If no experiment was active today, this resolves to nothing.
*
* Assumption: Only a single experiment can be active at a time.
*
* @return Promise<object>
*/
lastActiveToday: function () {
return Task.spawn(function* getMostRecentActiveExperimentTask() {
let experiments = yield this.getExperiments();
// Assumption: Ordered chronologically, descending, with active always
// first.
for (let experiment of experiments) {
if (experiment.active) {
return experiment;
}
if (experiment.endDate && this._dateIsTodayUTC(experiment.endDate)) {
return experiment;
}
}
return null;
}.bind(this));
},
/**
* Fetch an updated list of experiments and trigger experiment updates.
* Do only use when experiments are enabled.
@ -1432,3 +1473,105 @@ Experiments.ExperimentEntry.prototype = {
return true;
},
};
/**
* Strip a Date down to its UTC midnight.
*
* This will return a cloned Date object. The original is unchanged.
*/
let stripDateToMidnight = function (d) {
let m = new Date(d);
m.setUTCHours(0, 0, 0, 0);
return m;
};
function ExperimentsLastActiveMeasurement1() {
Metrics.Measurement.call(this);
}
const FIELD_DAILY_LAST_TEXT = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
ExperimentsLastActiveMeasurement1.prototype = Object.freeze({
__proto__: Metrics.Measurement.prototype,
name: "info",
version: 1,
fields: {
lastActive: FIELD_DAILY_LAST_TEXT,
}
});
this.ExperimentsProvider = function () {
Metrics.Provider.call(this);
this._experiments = null;
};
ExperimentsProvider.prototype = Object.freeze({
__proto__: Metrics.Provider.prototype,
name: "org.mozilla.experiments",
measurementTypes: [
ExperimentsLastActiveMeasurement1,
],
_OBSERVERS: [
OBSERVER_TOPIC,
],
postInit: function () {
this._experiments = Experiments.instance();
for (let o of this._OBSERVERS) {
Services.obs.addObserver(this, o, false);
}
return Promise.resolve();
},
onShutdown: function () {
for (let o of this._OBSERVERS) {
Services.obs.removeObserver(this, o);
}
return Promise.resolve();
},
observe: function (subject, topic, data) {
switch (topic) {
case OBSERVER_TOPIC:
this.recordLastActiveExperiment();
break;
}
},
collectDailyData: function () {
return this.recordLastActiveExperiment();
},
recordLastActiveExperiment: function () {
let m = this.getMeasurement(ExperimentsLastActiveMeasurement1.prototype.name,
ExperimentsLastActiveMeasurement1.prototype.version);
return this.enqueueStorageOperation(() => {
return Task.spawn(function* recordTask() {
let todayActive = yield this._experiments.lastActiveToday();
if (!todayActive) {
this._log.info("No active experiment on this day: " +
this._experiments._policy.now());
return;
}
this._log.info("Recording last active experiment: " + todayActive.id);
yield m.setDailyLastText("lastActive", todayActive.id,
this._experiments._policy.now());
}.bind(this));
});
},
});

View File

@ -2,3 +2,7 @@ component {f7800463-3b97-47f9-9341-b7617e6d8d49} ExperimentsService.js
contract @mozilla.org/browser/experiments-service;1 {f7800463-3b97-47f9-9341-b7617e6d8d49}
category update-timer ExperimentsService @mozilla.org/browser/experiments-service;1,getService,experiments-update-timer,experiments.manifest.fetchIntervalSeconds,86400
category profile-after-change ExperimentsService @mozilla.org/browser/experiments-service;1
category healthreport-js-provider-default ExperimentsProvider resource://gre/browser/modules/Experiments/Experiments.jsm

View File

@ -25,6 +25,35 @@ const EXPERIMENT2_ID = "test-experiment-2@tests.mozilla.org"
const EXPERIMENT2_XPI_SHA1 = "sha1:81877991ec70360fb48db84c34a9b2da7aa41d6a";
const EXPERIMENT2_XPI_NAME = "experiment-2.xpi";
const FAKE_EXPERIMENTS_1 = [
{
id: "id1",
name: "experiment1",
description: "experiment 1",
active: true,
detailUrl: "https://dummy/experiment1",
},
];
const FAKE_EXPERIMENTS_2 = [
{
id: "id2",
name: "experiment2",
description: "experiment 2",
active: false,
endDate: new Date(2014, 2, 11, 2, 4, 35, 42).getTime(),
detailUrl: "https://dummy/experiment2",
},
{
id: "id1",
name: "experiment1",
description: "experiment 1",
active: false,
endDate: new Date(2014, 2, 10, 0, 0, 0, 0).getTime(),
detailURL: "https://dummy/experiment1",
},
];
let gAppInfo = null;
function getReporter(name, uri, inspected) {
@ -129,3 +158,18 @@ function createAppInfo(options) {
registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo",
XULAPPINFO_CONTRACTID, XULAppInfoFactory);
}
/**
* Replace the experiments on an Experiments with a new list.
*
* This monkeypatches getExperiments(). It doesn't monkeypatch the internal
* experiments list. So its utility is not as great as it could be.
*/
function replaceExperiments(experiment, list) {
Object.defineProperty(experiment, "getExperiments", {
writable: true,
value: () => {
return Promise.resolve(list);
},
});
}

View File

@ -264,6 +264,33 @@ add_task(function* test_getExperiments() {
yield removeCacheFile();
});
add_task(function* test_lastActiveToday() {
let experiments = new Experiments.Experiments(gPolicy);
replaceExperiments(experiments, FAKE_EXPERIMENTS_1);
let e = yield experiments.getExperiments();
Assert.equal(e.length, 1, "Monkeypatch successful.");
Assert.equal(e[0].id, "id1", "ID looks sane");
Assert.ok(e[0].active, "Experiment is active.");
let lastActive = yield experiments.lastActiveToday();
Assert.equal(e[0], lastActive, "Last active object is expected.");
replaceExperiments(experiments, FAKE_EXPERIMENTS_2);
e = yield experiments.getExperiments();
Assert.equal(e.length, 2, "Monkeypatch successful.");
defineNow(gPolicy, e[0].endDate);
lastActive = yield experiments.lastActiveToday();
Assert.ok(lastActive, "Have a last active experiment");
Assert.equal(lastActive, e[0], "Last active object is expected.");
yield experiments.uninit();
yield removeCacheFile();
});
// Test explicitly disabling experiments.
add_task(function* test_disableExperiment() {
@ -636,6 +663,9 @@ add_task(function* test_userDisabledAndUpdated() {
Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, true, "Experiment 1 should be active.");
let todayActive = yield experiments.lastActiveToday();
Assert.ok(todayActive, "Last active for today reports a value.");
Assert.equal(todayActive.id, list[0].id, "The entry is what we expect.");
// Explicitly disable an experiment.
@ -649,6 +679,9 @@ add_task(function* test_userDisabledAndUpdated() {
Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, false, "Experiment should not be active anymore.");
todayActive = yield experiments.lastActiveToday();
Assert.ok(todayActive, "Last active for today still returns a value.");
Assert.equal(todayActive.id, list[0].id, "The ID is still the same.");
// Trigger an update with a faked change for experiment 1.
@ -718,6 +751,9 @@ add_task(function* test_updateActiveExperiment() {
let list = yield experiments.getExperiments();
Assert.equal(list.length, 0, "Experiment list should be empty.");
let todayActive = yield experiments.lastActiveToday();
Assert.equal(todayActive, null, "No experiment active today.");
// Trigger update, clock set for the experiment to start.
now = futureDate(startDate, 10 * MS_IN_ONE_DAY);
@ -731,6 +767,9 @@ add_task(function* test_updateActiveExperiment() {
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, true, "Experiment 1 should be active.");
Assert.equal(list[0].name, EXPERIMENT1_NAME, "Experiments name should match.");
todayActive = yield experiments.lastActiveToday();
Assert.ok(todayActive, "todayActive() returns a value.");
Assert.equal(todayActive.id, list[0].id, "It returns the active experiment.");
// Trigger an update for the active experiment by changing it's hash (and xpi)
// in the manifest.
@ -748,6 +787,8 @@ add_task(function* test_updateActiveExperiment() {
Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.equal(list[0].active, true, "Experiment 1 should still be active.");
Assert.equal(list[0].name, EXPERIMENT1A_NAME, "Experiments name should have been updated.");
todayActive = yield experiments.lastActiveToday();
Assert.equal(todayActive.id, list[0].id, "last active today is still sane.");
// Cleanup.

View File

@ -0,0 +1,110 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource:///modules/experiments/Experiments.jsm");
Cu.import("resource://testing-common/services/healthreport/utils.jsm");
Cu.import("resource://testing-common/services-common/logging.js");
function getStorageAndProvider(name) {
return Task.spawn(function* get() {
let storage = yield Metrics.Storage(name);
let provider = new ExperimentsProvider();
yield provider.init(storage);
return [storage, provider];
});
}
function run_test() {
do_get_profile();
initTestLogging();
run_next_test();
}
add_task(function test_constructor() {
let provider = new ExperimentsProvider();
});
add_task(function* test_init() {
let storage = yield Metrics.Storage("init");
let provider = new ExperimentsProvider();
yield provider.init(storage);
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_collect() {
let [storage, provider] = yield getStorageAndProvider("no_active");
// Initial state should not report anything.
yield provider.collectDailyData();
let m = provider.getMeasurement("info", 1);
let values = yield m.getValues();
Assert.equal(values.days.size, 0, "Have no data if no experiments known.");
// An old experiment that ended today should be reported.
replaceExperiments(provider._experiments, FAKE_EXPERIMENTS_2);
let now = new Date(FAKE_EXPERIMENTS_2[0].endDate);
defineNow(provider._experiments._policy, now.getTime());
yield provider.collectDailyData();
values = yield m.getValues();
Assert.equal(values.days.size, 1, "Have 1 day of data");
Assert.ok(values.days.hasDay(now), "Has day the experiment ended.");
let day = values.days.getDay(now);
Assert.ok(day.has("lastActive"), "Has lastActive field.");
Assert.equal(day.get("lastActive"), "id2", "Last active ID is sane.");
// Making an experiment active replaces the lastActive value.
replaceExperiments(provider._experiments, FAKE_EXPERIMENTS_1);
yield provider.collectDailyData();
values = yield m.getValues();
day = values.days.getDay(now);
Assert.equal(day.get("lastActive"), "id1", "Last active ID is the active experiment.");
// And make sure the observer works.
replaceExperiments(provider._experiments, FAKE_EXPERIMENTS_2);
Services.obs.notifyObservers(null, "experiments-changed", null);
// This may not wait long enough. It relies on the SQL insert happening
// in the same tick as the observer notification.
yield storage.enqueueOperation(function () {
return Promise.resolve();
});
values = yield m.getValues();
day = values.days.getDay(now);
Assert.equal(day.get("lastActive"), "id2", "Last active ID set by observer.");
yield provider.shutdown();
yield storage.close();
});
add_task(function* test_healthreporterJSON() {
let reporter = yield getHealthReporter("healthreporterJSON");
yield reporter.init();
try {
yield reporter._providerManager.registerProvider(new ExperimentsProvider());
let experiments = Experiments.instance();
defineNow(experiments._policy, Date.now());
replaceExperiments(experiments, FAKE_EXPERIMENTS_1);
yield reporter.collectMeasurements();
let payload = yield reporter.getJSONPayload(true);
let today = reporter._formatDate(reporter._policy.now());
Assert.ok(today in payload.data.days, "Day in payload.");
let day = payload.data.days[today];
Assert.ok("org.mozilla.experiments.info" in day, "Measurement present.");
let m = day["org.mozilla.experiments.info"];
Assert.ok("lastActive" in m, "lastActive field present.");
Assert.equal(m["lastActive"], "id1", "Last active ID proper.");
} finally {
reporter._shutdown();
}
});

View File

@ -12,3 +12,4 @@ support-files =
[test_api.js]
[test_conditions.js]
[test_fetch.js]
[test_healthreport.js]

View File

@ -202,6 +202,12 @@
- on the context menu that copies the selected request's url -->
<!ENTITY netmonitorUI.context.copyUrl "Copy URL">
<!-- LOCALIZATION NOTE (netmonitorUI.context.copyAsCurl): This is the label displayed
- on the context menu that copies the selected request as a cURL command.
- The capitalization is part of the official name and should be used throughout all languages.
- http://en.wikipedia.org/wiki/CURL -->
<!ENTITY netmonitorUI.context.copyAsCurl "Copy as cURL">
<!-- LOCALIZATION NOTE (netmonitorUI.context.copyUrl.accesskey): This is the access key
- for the Copy URL menu item displayed in the context menu for a request -->
<!ENTITY netmonitorUI.context.copyUrl.accesskey "C">

View File

@ -21,7 +21,7 @@
%include ../browser.inc
#PanelUI-popup #PanelUI-contents:empty {
height: 128px;
height: 128px;
}
#PanelUI-popup #PanelUI-contents:empty::before {
@ -328,6 +328,16 @@ toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"] > iframe {
margin: 4px auto;
}
#PanelUI-multiView[viewtype="subview"] > .panel-viewcontainer > .panel-viewstack > .panel-mainview > #PanelUI-mainView {
background-color: hsla(210,4%,10%,.1);
}
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .panel-wide-item,
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]),
#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer {
opacity: .5;
}
/*
* XXXgijs: this is a workaround for a layout issue that was caused by these iframes,
* which was affecting subview display. Because of this, we're hiding the iframe *only*

View File

@ -245,6 +245,10 @@ Leading by example::
"google": 1
},
"_v": "4"
},
"org.mozilla.experiment": {
"lastActive": "some.experiment.id"
"_v": "1"
}
}
}
@ -1461,3 +1465,29 @@ Example
"version": "12.2.0"
}
org.mozilla.experiments.info
----------------------------------
Daily measurement reporting information about the Telemetry Experiments service.
Version 1
^^^^^^^^^
Property:
lastActive
ID of the final Telemetry Experiment that is active on a given day, if any.
Example
^^^^^^^
::
"org.mozilla.experiments.info": {
"_v": 1,
"lastActive": "some.experiment.id"
}