mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge f-t to m-c
This commit is contained in:
commit
a6e09c5c74
@ -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))
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 ||
|
||||
|
@ -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;"/>
|
||||
|
@ -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]
|
||||
|
72
browser/devtools/netmonitor/test/browser_net_copy_as_curl.js
Normal file
72
browser/devtools/netmonitor/test/browser_net_copy_as_curl.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
232
browser/devtools/netmonitor/test/browser_net_curl-utils.js
Normal file
232
browser/devtools/netmonitor/test/browser_net_curl-utils.js
Normal 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¶m2=value2¶m3=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;
|
||||
});
|
||||
}
|
@ -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";
|
||||
|
27
browser/devtools/netmonitor/test/html_copy-as-curl.html
Normal file
27
browser/devtools/netmonitor/test/html_copy-as-curl.html
Normal 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>
|
99
browser/devtools/netmonitor/test/html_curl-utils.html
Normal file
99
browser/devtools/netmonitor/test/html_curl-utils.html
Normal 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¶m2=value2¶m3=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¶m2=value2¶m3=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>
|
396
browser/devtools/shared/Curl.jsm
Normal file
396
browser/devtools/shared/Curl.jsm
Normal 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, "\"^$&\"") + "\"";
|
||||
}
|
||||
};
|
@ -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));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
|
110
browser/experiments/test/xpcshell/test_healthreport.js
Normal file
110
browser/experiments/test/xpcshell/test_healthreport.js
Normal 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();
|
||||
}
|
||||
});
|
@ -12,3 +12,4 @@ support-files =
|
||||
[test_api.js]
|
||||
[test_conditions.js]
|
||||
[test_fetch.js]
|
||||
[test_healthreport.js]
|
||||
|
@ -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">
|
||||
|
@ -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*
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user