Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2016-01-28 12:27:53 +01:00
commit 2cd82dff43
58 changed files with 1763 additions and 201 deletions

View File

@ -7,6 +7,9 @@ var gTestBrowser = null;
var config = {};
add_task(function* () {
Services.prefs.setIntPref("media.gmp.log.level", 0);
Services.prefs.setBoolPref("extensions.logging.enabled", true);
// The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables plugin
// crash reports. This test needs them enabled. The test also needs a mock
// report server, and fortunately one is already set up by toolkit/
@ -26,6 +29,8 @@ add_task(function* () {
Services.prefs.setIntPref("dom.ipc.plugins.timeoutSecs", 0);
registerCleanupFunction(Task.async(function*() {
Services.prefs.clearUserPref("extensions.logging.enabled");
Services.prefs.clearUserPref("media.gmp.log.level");
Services.prefs.clearUserPref("dom.ipc.plugins.timeoutSecs");
env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport);
env.set("MOZ_CRASHREPORTER_URL", serverUrl);

View File

@ -448,14 +448,9 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
let options = {
js: [],
css: [],
};
// We need to send the inner window ID to make sure we only
// execute the script if the window is currently navigated to
// the document that we expect.
//
// TODO: When we add support for callbacks, non-matching
// window IDs and insufficient permissions need to result in a
// callback with |lastError| set.
let recipient = {
innerWindowID: tab.linkedBrowser.innerWindowID,
};
@ -487,17 +482,21 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => {
if (details.runAt !== null) {
options.run_at = details.runAt;
}
mm.sendAsyncMessage("Extension:Execute",
{extensionId: extension.id, options});
// TODO: Call the callback with the result (which is what???).
// TODO: Set lastError.
context.sendMessage(mm, "Extension:Execute", { options }, recipient)
.then(result => {
if (callback) {
runSafe(context, callback, result);
}
});
},
executeScript: function(tabId, details, callback) {
self.tabs._execute(tabId, details, "js", callback);
},
insertCss: function(tabId, details, callback) {
insertCSS: function(tabId, details, callback) {
self.tabs._execute(tabId, details, "css", callback);
},

View File

@ -20,8 +20,10 @@ support-files =
[browser_ext_popup_api_injection.js]
[browser_ext_contextMenus.js]
[browser_ext_getViews.js]
[browser_ext_tabs_executeScript.js]
[browser_ext_tabs_executeScript_good.js]
[browser_ext_tabs_executeScript_bad.js]
[browser_ext_tabs_insertCSS.js]
[browser_ext_tabs_query.js]
[browser_ext_tabs_getCurrent.js]
[browser_ext_tabs_create.js]

View File

@ -0,0 +1,66 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* testExecuteScript() {
let {MessageChannel} = Cu.import("resource://gre/modules/MessageChannel.jsm", {});
let messageManagersSize = MessageChannel.messageManagers.size;
let responseManagersSize = MessageChannel.responseManagers.size;
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true);
function background() {
browser.tabs.executeScript({
file: "script.js",
code: "42",
}, result => {
browser.test.assertEq(42, result, "Expected callback result");
browser.test.sendMessage("got result", result);
});
browser.tabs.executeScript({
file: "script2.js",
}, result => {
browser.test.assertEq(27, result, "Expected callback result");
browser.test.sendMessage("got callback", result);
});
browser.runtime.onMessage.addListener(message => {
browser.test.assertEq("script ran", message, "Expected runtime message");
browser.test.sendMessage("got message", message);
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": ["http://mochi.test/"],
},
background,
files: {
"script.js": function() {
browser.runtime.sendMessage("script ran");
},
"script2.js": "27",
},
});
yield extension.startup();
yield extension.awaitMessage("got result");
yield extension.awaitMessage("got callback");
yield extension.awaitMessage("got message");
yield extension.unload();
yield BrowserTestUtils.removeTab(tab);
// Make sure that we're not holding on to references to closed message
// managers.
is(MessageChannel.messageManagers.size, messageManagersSize, "Message manager count");
is(MessageChannel.responseManagers.size, responseManagersSize, "Response manager count");
is(MessageChannel.pendingResponses.size, 0, "Pending response count");
});

View File

@ -0,0 +1,106 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
add_task(function* testExecuteScript() {
let {MessageChannel} = Cu.import("resource://gre/modules/MessageChannel.jsm", {});
let messageManagersSize = MessageChannel.messageManagers.size;
let responseManagersSize = MessageChannel.responseManagers.size;
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true);
function background() {
let promises = [
{
background: "rgb(0, 0, 0)",
foreground: "rgb(255, 192, 203)",
promise: resolve => {
browser.tabs.insertCSS({
file: "file1.css",
code: "* { background: black }",
}, result => {
browser.test.assertEq(undefined, result, "Expected callback result");
resolve();
});
},
},
{
background: "rgb(0, 0, 0)",
foreground: "rgb(0, 113, 4)",
promise: resolve => {
browser.tabs.insertCSS({
file: "file2.css",
}, result => {
browser.test.assertEq(undefined, result, "Expected callback result");
resolve();
});
},
},
{
background: "rgb(42, 42, 42)",
foreground: "rgb(0, 113, 4)",
promise: resolve => {
browser.tabs.insertCSS({
code: "* { background: rgb(42, 42, 42) }",
}, result => {
browser.test.assertEq(undefined, result, "Expected callback result");
resolve();
});
},
},
];
function checkCSS() {
let computedStyle = window.getComputedStyle(document.body);
return [computedStyle.backgroundColor, computedStyle.color];
}
function next() {
if (!promises.length) {
browser.test.notifyPass("insertCSS");
return;
}
let { promise, background, foreground } = promises.shift();
new Promise(promise).then(() => {
browser.tabs.executeScript({
code: `(${checkCSS})()`,
}, result => {
browser.test.assertEq(background, result[0], "Expected background color");
browser.test.assertEq(foreground, result[1], "Expected foreground color");
next();
});
});
}
next();
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": ["http://mochi.test/"],
},
background,
files: {
"file1.css": "* { color: pink }",
"file2.css": "* { color: rgb(0, 113, 4) }",
},
});
yield extension.startup();
yield extension.awaitFinish("insertCSS");
yield extension.unload();
yield BrowserTestUtils.removeTab(tab);
// Make sure that we're not holding on to references to closed message
// managers.
is(MessageChannel.messageManagers.size, messageManagersSize, "Message manager count");
is(MessageChannel.responseManagers.size, responseManagersSize, "Response manager count");
is(MessageChannel.pendingResponses.size, 0, "Pending response count");
});

View File

@ -213,15 +213,15 @@
</vbox>
</hbox>
<label class="fxaMobilePromo">
&mobilePromo2.start;<!-- We put these comments to avoid inserting white spaces
&mobilePromo3.start;<!-- We put these comments to avoid inserting white spaces
--><label id="fxaMobilePromo-android"
class="androidLink text-link"><!--
-->&mobilePromo2.androidLink;</label><!--
-->&mobilePromo2.iOSBefore;<!--
-->&mobilePromo3.androidLink;</label><!--
-->&mobilePromo3.iOSBefore;<!--
--><label id="fxaMobilePromo-ios"
class="iOSLink text-link"><!--
-->&mobilePromo2.iOSLink;</label><!--
-->&mobilePromo2.end;
-->&mobilePromo3.iOSLink;</label><!--
-->&mobilePromo3.end;
</label>
</vbox>
@ -347,15 +347,15 @@
</hbox>
</groupbox>
<label class="fxaMobilePromo">
&mobilePromo2.start;<!-- We put these comments to avoid inserting white spaces
&mobilePromo3.start;<!-- We put these comments to avoid inserting white spaces
--><label class="androidLink text-link"
href="https://www.mozilla.org/firefox/android/?utm_source=firefox-browser&amp;utm_medium=firefox-browser&amp;utm_campaign=sync-preferences"><!--
-->&mobilePromo2.androidLink;</label><!--
-->&mobilePromo2.iOSBefore;<!--
-->&mobilePromo3.androidLink;</label><!--
-->&mobilePromo3.iOSBefore;<!--
--><label class="iOSLink text-link"
href="https://www.mozilla.org/firefox/ios/?utm_source=firefox-browser&amp;utm_medium=firefox-browser&amp;utm_campaign=sync-preferences"><!--
-->&mobilePromo2.iOSLink;</label><!--
-->&mobilePromo2.end;
-->&mobilePromo3.iOSLink;</label><!--
-->&mobilePromo3.end;
</label>
<vbox id="tosPP-small" align="start">
<label id="tosPP-small-ToS" class="text-link">

View File

@ -220,17 +220,6 @@ if (typeof Mozilla == 'undefined') {
});
};
Mozilla.UITour.startUrlbarCapture = function(text, url) {
_sendEvent('startUrlbarCapture', {
text: text,
url: url
});
};
Mozilla.UITour.endUrlbarCapture = function() {
_sendEvent('endUrlbarCapture');
};
Mozilla.UITour.getConfiguration = function(configName, callback) {
_sendEvent('getConfiguration', {
callbackID: _waitForCallback(callback),

View File

@ -43,7 +43,6 @@ const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs";
const PREF_READERVIEW_TRIGGER = "browser.uitour.readerViewTrigger";
const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([
"endUrlbarCapture",
"forceShowReaderIcon",
"getConfiguration",
"getTreatmentTag",
@ -90,7 +89,6 @@ this.UITour = {
pageIDSourceBrowsers: new WeakMap(),
/* Map from browser chrome windows to a Set of <browser>s in which a tour is open (both visible and hidden) */
tourBrowsersByWindow: new WeakMap(),
urlbarCapture: new WeakMap(),
appMenuOpenForAnnotation: new Set(),
availableTargetsCache: new WeakMap(),
clearAvailableTargetsCache() {
@ -577,41 +575,6 @@ this.UITour = {
break;
}
case "startUrlbarCapture": {
if (typeof data.text != "string" || !data.text ||
typeof data.url != "string" || !data.url) {
log.warn("startUrlbarCapture: Text or URL not specified");
return false;
}
let uri = null;
try {
uri = Services.io.newURI(data.url, null, null);
} catch (e) {
log.warn("startUrlbarCapture: Malformed URL specified");
return false;
}
let secman = Services.scriptSecurityManager;
let contentDocument = browser.contentWindow.document;
let principal = contentDocument.nodePrincipal;
let flags = secman.DISALLOW_INHERIT_PRINCIPAL;
try {
secman.checkLoadURIWithPrincipal(principal, uri, flags);
} catch (e) {
log.warn("startUrlbarCapture: Orginating page doesn't have permission to open specified URL");
return false;
}
this.startUrlbarCapture(window, data.text, data.url);
break;
}
case "endUrlbarCapture": {
this.endUrlbarCapture(window);
break;
}
case "getConfiguration": {
if (typeof data.configuration != "string") {
log.warn("getConfiguration: No configuration option specified");
@ -698,8 +661,8 @@ this.UITour = {
targetPromise.then(target => {
let searchbar = target.node;
searchbar.value = data.term;
searchbar.inputChanged();
}).then(null, Cu.reportError);
searchbar.updateGoButtonVisibility();
});
break;
}
@ -806,14 +769,6 @@ this.UITour = {
this.teardownTourForWindow(window);
break;
}
case "input": {
if (aEvent.target.id == "urlbar") {
let window = aEvent.target.ownerDocument.defaultView;
this.handleUrlbarInput(window);
}
break;
}
}
},
@ -903,7 +858,6 @@ this.UITour = {
controlCenterPanel.removeEventListener("popuphidden", this.onPanelHidden);
controlCenterPanel.removeEventListener("popuphiding", this.hideControlCenterAnnotations);
this.endUrlbarCapture(aWindow);
this.resetTheme();
// If there are no more tour tabs left in the window, teardown the tour for the whole window.
@ -1763,41 +1717,6 @@ this.UITour = {
aPanel.hidden = false;
},
startUrlbarCapture: function(aWindow, aExpectedText, aUrl) {
let urlbar = aWindow.document.getElementById("urlbar");
this.urlbarCapture.set(aWindow, {
expected: aExpectedText.toLocaleLowerCase(),
url: aUrl
});
urlbar.addEventListener("input", this);
},
endUrlbarCapture: function(aWindow) {
let urlbar = aWindow.document.getElementById("urlbar");
urlbar.removeEventListener("input", this);
this.urlbarCapture.delete(aWindow);
},
handleUrlbarInput: function(aWindow) {
if (!this.urlbarCapture.has(aWindow))
return;
let urlbar = aWindow.document.getElementById("urlbar");
let {expected, url} = this.urlbarCapture.get(aWindow);
if (urlbar.value.toLocaleLowerCase().localeCompare(expected) != 0)
return;
urlbar.handleRevert();
let tab = aWindow.gBrowser.addTab(url, {
owner: aWindow.gBrowser.selectedTab,
relatedToCurrent: true
});
aWindow.gBrowser.selectedTab = tab;
},
getConfiguration: function(aMessageManager, aWindow, aConfiguration, aCallbackID) {
switch (aConfiguration) {
case "appinfo":

View File

@ -91,18 +91,18 @@ both, to better adapt this sentence to their language.
<!ENTITY signedIn.engines.label "Sync across all devices">
<!-- LOCALIZATION NOTE (mobilePromo2.*): the following strings will be used to
<!-- LOCALIZATION NOTE (mobilePromo3.*): the following strings will be used to
create a single sentence with active links.
The resulting sentence in English is: "Sync to your mobile device.
Download Firefox for Android or Firefox for iOS." -->
The resulting sentence in English is: "Download Firefox for
Android or iOS to sync with your mobile device." -->
<!ENTITY mobilePromo2.start "Sync to your mobile device. Download ">
<!-- LOCALIZATION NOTE (mobilePromo2.androidLink): This is a link title that links to https://www.mozilla.org/firefox/android/ -->
<!ENTITY mobilePromo2.androidLink "Firefox for Android">
<!ENTITY mobilePromo3.start "Download Firefox for ">
<!-- LOCALIZATION NOTE (mobilePromo3.androidLink): This is a link title that links to https://www.mozilla.org/firefox/android/ -->
<!ENTITY mobilePromo3.androidLink "Android">
<!-- LOCALIZATION NOTE (mobilePromo2.iOSBefore): This is text displayed between mobilePromo2.androidLink and mobilePromo2.iosLink -->
<!ENTITY mobilePromo2.iOSBefore " or ">
<!-- LOCALIZATION NOTE (mobilePromo2.iOSLink): This is a link title that links to https://www.mozilla.org/firefox/ios/ -->
<!ENTITY mobilePromo2.iOSLink "Firefox for iOS">
<!-- LOCALIZATION NOTE (mobilePromo3.iOSBefore): This is text displayed between mobilePromo3.androidLink and mobilePromo3.iosLink -->
<!ENTITY mobilePromo3.iOSBefore " or ">
<!-- LOCALIZATION NOTE (mobilePromo3.iOSLink): This is a link title that links to https://www.mozilla.org/firefox/ios/ -->
<!ENTITY mobilePromo3.iOSLink "iOS">
<!ENTITY mobilePromo2.end ".">
<!ENTITY mobilePromo3.end " to sync with your mobile device.">

View File

@ -96,7 +96,6 @@ body {
/* Advanced section is hidden via inline styles until the link is clicked */
#advancedPanel {
position: absolute;
background-color: white;
color: var(--in-content-text-color);
border: 1px lightgray solid;

View File

@ -154,7 +154,6 @@ div#weakCryptoAdvancedPanel {
padding: 0 12px 12px 12px;
box-shadow: 0 0 4px #ddd;
font-size: 0.9em;
position: absolute;
}
#overrideWeakCryptoPanel {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

After

Width:  |  Height:  |  Size: 719 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -563,7 +563,8 @@ description > html|a {
}
.fxaMobilePromo {
margin-bottom: 31px;
margin-bottom: 20px;
margin-top: 25px;
}
#fxaLoginRejectedWarning {
@ -576,9 +577,22 @@ description > html|a {
margin-bottom: 27.5px;
}
.androidLink {
background-image: url("chrome://browser/skin/fxa/android.png");
}
.iOSLink {
background-image: url("chrome://browser/skin/fxa/ios.png");
}
.androidLink,
.iOSLink {
margin: 0;
margin: 0 0 0 2px;
padding-left: 28px;
padding-top: 6px;
height: 28px;
background-repeat: no-repeat;
background-size: 24px 28px;
}
#tosPP-small {
@ -587,6 +601,12 @@ description > html|a {
}
@media (min-resolution: 1.1dppx) {
.androidLink {
background-image: url("chrome://browser/skin/fxa/android@2x.png");
}
.iOSLink {
background-image: url("chrome://browser/skin/fxa/ios@2x.png");
}
.fxaSyncIllustration {
list-style-image: url(chrome://browser/skin/fxa/sync-illustration@2x.png)
}

View File

@ -81,6 +81,8 @@
skin/classic/browser/fxa/sync-illustration.svg (../shared/fxa/sync-illustration.svg)
skin/classic/browser/fxa/android.png (../shared/fxa/android.png)
skin/classic/browser/fxa/android@2x.png (../shared/fxa/android@2x.png)
skin/classic/browser/fxa/ios.png (../shared/fxa/ios.png)
skin/classic/browser/fxa/ios@2x.png (../shared/fxa/ios@2x.png)
skin/classic/browser/syncedtabs/twisty-closed.svg (../shared/syncedtabs/twisty-closed.svg)
skin/classic/browser/syncedtabs/twisty-open.svg (../shared/syncedtabs/twisty-open.svg)
skin/classic/browser/search-pref.png (../shared/search/search-pref.png)

View File

@ -47,8 +47,6 @@
<div class="header">
<h1 class="header-name">&aboutDebugging.workers;</h1>
</div>
<input id="enable-worker-debugging" type="checkbox" data-pref="devtools.debugger.workers"/>
<label for="enable-worker-debugging" title="&options.enableWorkers.tooltip;">&options.enableWorkers.label;</label>
<div id="workers"></div>
</div>
</div>

View File

@ -77,3 +77,9 @@
.gcli-row-out .nowrap {
white-space: nowrap;
}
.gcli-mdn-url {
text-decoration: underline;
cursor: pointer;
}

View File

@ -145,5 +145,6 @@
<splitter id="toolbox-console-splitter" class="devtools-horizontal-splitter" hidden="true" />
<box minheight="75" flex="1" id="toolbox-panel-webconsole" collapsed="true" />
</vbox>
<tooltip id="aHTMLTooltip" page="true" />
</notificationbox>
</window>

View File

@ -314,6 +314,7 @@ devtools.jar:
skin/images/tool-memory-active.svg (themes/images/tool-memory-active.svg)
skin/images/close.png (themes/images/close.png)
skin/images/close@2x.png (themes/images/close@2x.png)
skin/images/clear.svg (themes/images/clear.svg)
skin/images/vview-delete.png (themes/images/vview-delete.png)
skin/images/vview-delete@2x.png (themes/images/vview-delete@2x.png)
skin/images/vview-edit.png (themes/images/vview-edit.png)

View File

@ -84,9 +84,9 @@ take-snapshot=Take snapshot
# initiates importing a snapshot.
import-snapshot=Import…
# LOCALIZATION NOTE (clear-snapshots): The label describing the button that clears
# existing snapshot.
clear-snapshots=Clear
# LOCALIZATION NOTE (clear-snapshots.tooltip): The tooltip for the button that
# deletes existing snapshot.
clear-snapshots.tooltip=Delete all snapshots
# LOCALIZATION NOTE (diff-snapshots): The label for the button that initiates
# selecting two snapshots to diff with each other.

View File

@ -196,11 +196,10 @@ const Toolbar = module.exports = createClass({
dom.button({
id: "clear-snapshots",
className: "devtools-toolbarbutton clear-snapshots devtools-button",
className: "clear-snapshots devtools-button",
onClick: onClearSnapshotsClick,
title: L10N.getStr("clear-snapshots"),
"data-text-only": true,
}, L10N.getStr("clear-snapshots"))
title: L10N.getStr("clear-snapshots.tooltip")
})
),
dom.label(

View File

@ -29,8 +29,11 @@ module.exports = createClass({
// handles, etc.
return dom.div(
{
className: "viewport",
className: "viewport"
},
dom.div({
className: "viewport-header",
}),
Browser({
location,
width: viewport.width,

View File

@ -1,6 +1,33 @@
/* TODO: May break up into component local CSS. Pending future discussions by
* React component group on how to best handle CSS. */
iframe {
html, body, #app, #viewports {
height: 100%;
}
#viewports {
display: flex;
justify-content: center;
align-items: center;
}
/**
* Viewport Container
*/
.viewport {
border: 1px solid var(--theme-splitter-color);
box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
}
.viewport-header {
background-color: var(--theme-toolbar-background);
border-bottom: 1px solid var(--theme-splitter-color);
color: var(--theme-body-color-alt);
height: 16px;
}
.browser {
display: block;
border: 0;
}

View File

@ -41,6 +41,7 @@ var XHR_CSS_URL = "https://developer.mozilla.org/en-US/docs/Web/CSS/";
const PAGE_LINK_PARAMS = "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default"
// URL for the page link omits locale, so a locale-specific page will be loaded
var PAGE_LINK_URL = "https://developer.mozilla.org/docs/Web/CSS/";
exports.PAGE_LINK_URL = PAGE_LINK_URL;
const BROWSER_WINDOW = 'navigator:browser';

View File

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#babec3">
<path d="M8,1.1c-3.8,0-6.9,3-6.9,6.9s3,6.9,6.9,6.9s6.9-3,6.9-6.9S11.8,1.1,8,1.1z M13.1,7.9c0,1.1-0.4,2.1-1,3L5,3.8 c0.8-0.6,1.9-1,3-1C10.8,2.8,13.1,5.1,13.1,7.9z M2.9,7.9c0-1.1,0.3-2.1,0.9-2.9l7.1,7.1C10.1,12.7,9.1,13,8,13 C5.2,13,2.9,10.7,2.9,7.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@ -102,6 +102,11 @@ html, body, #app, #memory-tool {
#take-snapshot::before {
background-image: url(images/command-screenshot.png);
}
#clear-snapshots::before {
background-image: url(chrome://devtools/skin/images/clear.svg);
}
@media (min-resolution: 1.1dppx) {
#take-snapshot::before {
background-image: url(images/command-screenshot@2x.png);
@ -233,6 +238,7 @@ html, body, #app, #memory-tool {
* (https://drafts.csswg.org/css-flexbox/#min-size-auto)
*/
min-width: 0;
font-size: 90%;
}
#heap-view > .heap-view-panel {
@ -294,7 +300,6 @@ html, body, #app, #memory-tool {
text-overflow: ellipsis;
line-height: var(--heap-tree-header-height);
justify-content: center;
font-size: 90%;
white-space: nowrap;
}
@ -317,6 +322,7 @@ html, body, #app, #memory-tool {
.tree-node {
height: var(--heap-tree-row-height);
line-height: var(--heap-tree-row-height);
}
/**
@ -363,6 +369,12 @@ html, body, #app, #memory-tool {
.heap-tree-item-bytes,
.heap-tree-item-total-bytes {
width: 10%;
/*
* Provision for up to :
* - 12 characters for the number part (10s of GB and spaces every 3 digits)
* - 4 chars for the percent part (the maximum length string is "100%")
*/
min-width: 16ch;
}
.heap-tree-item-name {
@ -410,7 +422,7 @@ html, body, #app, #memory-tool {
}
.heap-tree-percent {
width: 2.5em;
width: 4ch;
}
.heap-tree-item.focused .heap-tree-number,
@ -487,9 +499,9 @@ html, body, #app, #memory-tool {
}
.no-allocation-stacks {
border-color: var(--theme-splitter-color);
border-style: solid;
border-width: 0px 0px 1px 0px;
text-align: center;
padding: 5px;
border-color: var(--theme-splitter-color);
border-style: solid;
border-width: 0px 0px 1px 0px;
text-align: center;
padding: 5px;
}

View File

@ -54,7 +54,7 @@ add_task(function* () {
function reload(browser) {
let loaded = loadBrowser(browser);
content.location.reload();
browser.reload();
return loaded;
}

View File

@ -65,6 +65,7 @@ exports.devtoolsModules = [
"devtools/shared/gcli/commands/inject",
"devtools/shared/gcli/commands/jsb",
"devtools/shared/gcli/commands/listen",
"devtools/shared/gcli/commands/mdn",
"devtools/shared/gcli/commands/measure",
"devtools/shared/gcli/commands/media",
"devtools/shared/gcli/commands/pagemod",

View File

@ -0,0 +1,76 @@
/* 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/. */
"use strict";
const l10n = require("gcli/l10n");
const {
getCssDocs,
PAGE_LINK_URL
} = require("devtools/client/shared/widgets/MdnDocsWidget");
exports.items = [{
name: "mdn",
description: l10n.lookup("mdnDesc")
}, {
item: "command",
runAt: "client",
name: "mdn css",
description: l10n.lookup("mdnCssDesc"),
returnType: "cssPropertyOutput",
params: [{
name: "property",
type: { name: "string" },
defaultValue: null,
description: l10n.lookup("mdnCssProp")
}],
exec: function(args) {
return getCssDocs(args.property).then(result => {
return {
data: result,
url: PAGE_LINK_URL + args.property,
property: args.property
};
}, error => {
return { error, property: args.property };
});
}
}, {
item: "converter",
from: "cssPropertyOutput",
to: "dom",
exec: function(result, context) {
let propertyName = result.property;
let document = context.document;
let root = document.createElement("div");
if (result.error) {
// The css property specified doesn't exist.
root.appendChild(document.createTextNode(
l10n.lookupFormat("mdnCssPropertyNotFound", [ propertyName ]) +
" (" + result.error + ")"));
} else {
let title = document.createElement("h2");
title.textContent = propertyName;
root.appendChild(title);
let link = document.createElement("p");
link.classList.add("gcli-mdn-url");
link.textContent = l10n.lookup("mdnCssVisitPage");
root.appendChild(link);
link.addEventListener("click", () => {
let gBrowser = context.environment.chromeWindow.gBrowser;
gBrowser.selectedTab = gBrowser.addTab(result.url);
});
let summary = document.createElement("p");
summary.textContent = result.data.summary;
root.appendChild(summary);
}
return root;
}
}];

View File

@ -1612,6 +1612,24 @@ folderInvalidPath=Please enter a valid path
# The argument (%1$S) is the folder path.
folderOpenDirResult=Opened %1$S
# LOCALIZATION NOTE (mdnDesc) A very short string used to describe the
# use of 'mdn' command.
mdnDesc=Retrieve documentation from MDN
# LOCALIZATION NOTE (mdnCssDesc) A very short string used to describe the
# result of the 'mdn css' commmand.
mdnCssDesc=Retrieve documentation about a given CSS property name from MDN
# LOCALIZATION NOTE (mdnCssProp) String used to describe the 'property name'
# parameter used in the 'mdn css' command.
mdnCssProp=Property name
# LOCALIZATION NOTE (mdnCssPropertyNotFound) String used to display an error in
# the result of the 'mdn css' command. Errors occur when a given CSS property
# wasn't found on MDN. The %1$S parameter will be replaced with the name of the
# CSS property.
mdnCssPropertyNotFound=MDN documentation for the CSS property '%1$S' was not found.
# LOCALIZATION NOTE (mdnCssVisitPage) String used as the label of a link to the
# MDN page for a given CSS property.
mdnCssVisitPage=Visit MDN page
# LOCALIZATION NOTE (security)
securityPrivacyDesc=Display supported security and privacy features
securityManual=Commands to list and get suggestions about security features for the current domain.

View File

@ -21,6 +21,11 @@ function doConsoleCalls(aState)
let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 2)).join("a");
top.console.log("foobarBaz-log", undefined);
top.console.log("Float from not a number: %f", "foo");
top.console.log("Float from string: %f", "1.2");
top.console.log("Float from number: %f", 1.3);
top.console.info("foobarBaz-info", null);
top.console.warn("foobarBaz-warn", top.document.documentElement);
top.console.debug(null);
@ -52,6 +57,18 @@ function doConsoleCalls(aState)
timeStamp: /^\d+$/,
arguments: ["foobarBaz-log", { type: "undefined" }],
},
{
level: "log",
arguments: ["Float from not a number: NaN"],
},
{
level: "log",
arguments: ["Float from string: 1.200000"],
},
{
level: "log",
arguments: ["Float from number: 1.300000"],
},
{
level: "info",
filename: /test_consoleapi/,

View File

@ -1595,9 +1595,14 @@ Console::ProcessArguments(JSContext* aCx,
return false;
}
nsCString format;
MakeFormatString(format, integer, mantissa, 'f');
output.AppendPrintf(format.get(), v);
// nspr returns "nan", but we want to expose it as "NaN"
if (std::isnan(v)) {
output.AppendFloat(v);
} else {
nsCString format;
MakeFormatString(format, integer, mantissa, 'f');
output.AppendPrintf(format.get(), v);
}
}
break;

View File

@ -469,6 +469,9 @@
android:name="org.mozilla.gecko.dlc.DownloadContentService">
</service>
<service
android:name="org.mozilla.gecko.telemetry.TelemetryUploadService"
android:exported="false"/>
#include ../services/manifests/FxAccountAndroidManifest_services.xml.in

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko;
import android.Manifest;
import android.os.AsyncTask;
import org.mozilla.gecko.adjust.AdjustHelperInterface;
import org.mozilla.gecko.annotation.RobocopTarget;
@ -46,6 +47,7 @@ import org.mozilla.gecko.menu.GeckoMenuItem;
import org.mozilla.gecko.mozglue.ContextUtils;
import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.permissions.Permissions;
import org.mozilla.gecko.preferences.ClearOnShutdownPref;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.prompts.Prompt;
@ -60,11 +62,14 @@ import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
import org.mozilla.gecko.tabs.TabHistoryFragment;
import org.mozilla.gecko.tabs.TabHistoryPage;
import org.mozilla.gecko.tabs.TabsPanel;
import org.mozilla.gecko.telemetry.TelemetryConstants;
import org.mozilla.gecko.telemetry.TelemetryUploadService;
import org.mozilla.gecko.toolbar.AutocompleteHandler;
import org.mozilla.gecko.toolbar.BrowserToolbar;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.toolbar.ToolbarProgressView;
import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
import org.mozilla.gecko.updater.UpdateServiceHelper;
import org.mozilla.gecko.util.ActivityUtils;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.EventCallback;
@ -156,6 +161,7 @@ import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.Vector;
public class BrowserApp extends GeckoApp
@ -754,6 +760,39 @@ public class BrowserApp extends GeckoApp
// Set the maximum bits-per-pixel the favicon system cares about.
IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
// The update service is enabled for RELEASE_BUILD, which includes the release and beta channels.
// However, no updates are served. Therefore, we don't trust the update service directly, and
// try to avoid prompting unnecessarily. See Bug 1232798.
if (!AppConstants.RELEASE_BUILD && UpdateServiceHelper.isUpdaterEnabled()) {
Permissions.from(this)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.doNotPrompt()
.andFallback(new Runnable() {
@Override
public void run() {
showUpdaterPermissionSnackbar();
}
})
.run();
}
}
private void showUpdaterPermissionSnackbar() {
SnackbarHelper.SnackbarCallback allowCallback = new SnackbarHelper.SnackbarCallback() {
@Override
public void onClick(View v) {
Permissions.from(BrowserApp.this)
.withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.run();
}
};
SnackbarHelper.showSnackbarWithAction(this,
getString(R.string.updater_permission_text),
Snackbar.LENGTH_INDEFINITE,
getString(R.string.updater_permission_allow),
allowCallback);
}
private void conditionallyNotifyHCEOL() {
@ -958,7 +997,8 @@ public class BrowserApp extends GeckoApp
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
if (getProfile().inGuestMode()) {
final GeckoProfile profile = getProfile();
if (profile.inGuestMode()) {
GuestSession.showNotification(BrowserApp.this);
} else {
// If we're restarting, we won't destroy the activity.
@ -966,6 +1006,15 @@ public class BrowserApp extends GeckoApp
// have been shown.
GuestSession.hideNotification(BrowserApp.this);
}
// We don't upload in onCreate because that's only called when the Activity needs to be instantiated
// and it's possible the system will never free the Activity from memory.
//
// We don't upload in onResume/onPause because that will be called each time the Activity is obscured,
// including by our own Activities/dialogs, and there is no reason to upload each time we're unobscured.
//
// So we're left with onStart/onStop.
uploadTelemetry(profile);
}
});
}
@ -3883,6 +3932,26 @@ public class BrowserApp extends GeckoApp
mDynamicToolbar.setTemporarilyVisible(false, VisibilityTransition.IMMEDIATE);
}
private void uploadTelemetry(final GeckoProfile profile) {
if (!TelemetryConstants.UPLOAD_ENABLED || profile.inGuestMode()) {
return;
}
final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(this, profile.getName());
final int seq = sharedPrefs.getInt(TelemetryConstants.PREF_SEQ_COUNT, 1);
final Intent i = new Intent(TelemetryConstants.ACTION_UPLOAD_CORE);
i.setClass(this, TelemetryUploadService.class);
i.putExtra(TelemetryConstants.EXTRA_DOC_ID, UUID.randomUUID().toString());
i.putExtra(TelemetryConstants.EXTRA_PROFILE_NAME, profile.getName());
i.putExtra(TelemetryConstants.EXTRA_PROFILE_PATH, profile.getDir().toString());
i.putExtra(TelemetryConstants.EXTRA_SEQ, seq);
startService(i);
// Intent redelivery will ensure this value gets used - see TelemetryUploadService class comments for details.
sharedPrefs.edit().putInt(TelemetryConstants.PREF_SEQ_COUNT, seq + 1).apply();
}
public static interface Refreshable {
public void refresh();
}

View File

@ -38,12 +38,18 @@ import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
public final class GeckoProfile {
private static final String LOGTAG = "GeckoProfile";
// The path in the profile to the file containing the client ID.
private static final String CLIENT_ID_FILE_PATH = "datareporting/state.json";
// In the client ID file, the attribute title in the JSON object containing the client ID value.
private static final String CLIENT_ID_JSON_ATTR = "clientID";
// Only tests should need to do this.
// We can default this to AppConstants.RELEASE_BUILD once we fix Bug 1069687.
private static volatile boolean sAcceptDirectoryChanges = true;
@ -588,6 +594,39 @@ public final class GeckoProfile {
return new File(f, aFile);
}
/**
* Retrieves the Gecko client ID from the filesystem.
*
* This method assumes the client ID is located in a file at a hard-coded path within the profile. The format of
* this file is a JSONObject which at the bottom level contains a String -> String mapping containing the client ID.
*
* WARNING: the platform provides a JSM to retrieve the client ID [1] and this would be a
* robust way to access it. However, we don't want to rely on Gecko running in order to get
* the client ID so instead we access the file this module accesses directly. However, it's
* possible the format of this file (and the access calls in the jsm) will change, leaving
* this code to fail.
*
* TODO: Write tests to prevent regressions. Mention them here. Test both file location and file format.
*
* [1]: https://mxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm
*/
@WorkerThread
public String getClientId() throws IOException {
final String clientIdFileContents;
try {
clientIdFileContents = readFile(CLIENT_ID_FILE_PATH);
} catch (final IOException e) {
throw new IOException("Could not read client ID file to retrieve client ID", e);
}
try {
final org.json.JSONObject json = new org.json.JSONObject(clientIdFileContents);
return json.getString(CLIENT_ID_JSON_ATTR);
} catch (final JSONException e) {
throw new IOException("Could not parse JSON to retrieve client ID", e);
}
}
/**
* Moves the session file to the backup session file.
*

View File

@ -1,4 +1,7 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko.dlc;

View File

@ -65,11 +65,18 @@ public class PermissionBlock {
return this;
}
/**
* Execute this permission block. Calling this method will prompt the user if needed.
*/
public void run() {
run(null);
}
/**
* Execute the specified runnable if the app has been granted all permissions. Calling this method will prompt the
* user if needed.
*/
public void run(@NonNull Runnable onPermissionsGranted) {
public void run(Runnable onPermissionsGranted) {
if (!doNotPrompt && !(context instanceof Activity)) {
throw new IllegalStateException("You need to either specify doNotPrompt() or pass in an Activity context");
}

View File

@ -0,0 +1,44 @@
/* 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/. */
package org.mozilla.gecko.telemetry;
import org.mozilla.gecko.AppConstants;
public class TelemetryConstants {
// Change these two values to enable upload in developer builds.
public static final boolean UPLOAD_ENABLED = AppConstants.MOZILLA_OFFICIAL; // Disabled for developer builds.
public static final String DEFAULT_SERVER_URL = "https://incoming.telemetry.mozilla.org";
public static final String USER_AGENT =
"Firefox-Android-Telemetry/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
public static final String ACTION_UPLOAD_CORE = "uploadCore";
public static final String EXTRA_DOC_ID = "docId";
public static final String EXTRA_PROFILE_NAME = "geckoProfileName";
public static final String EXTRA_PROFILE_PATH = "geckoProfilePath";
public static final String EXTRA_SEQ = "seq";
public static final String PREF_SERVER_URL = "telemetry-serverUrl";
public static final String PREF_SEQ_COUNT = "telemetry-seqCount";
public static class CorePing {
private CorePing() { /* To prevent instantiation */ }
public static final String NAME = "core";
public static final int VERSION_VALUE = 1;
public static final String OS_VALUE = "Android";
public static final String ARCHITECTURE = "arch";
public static final String CLIENT_ID = "clientId";
public static final String DEVICE = "device";
public static final String EXPERIMENTS = "experiments";
public static final String LOCALE = "locale";
public static final String OS_ATTR = "os";
public static final String OS_VERSION = "osversion";
public static final String SEQ = "seq";
public static final String VERSION_ATTR = "v";
}
}

View File

@ -0,0 +1,24 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko.telemetry;
import org.mozilla.gecko.sync.ExtendedJSONObject;
/**
* Container for telemetry data and the data necessary to upload it.
*/
public class TelemetryPing {
private final String url;
private final ExtendedJSONObject payload;
public TelemetryPing(final String url, final ExtendedJSONObject payload) {
this.url = url;
this.payload = payload;
}
public String getURL() { return url; }
public ExtendedJSONObject getPayload() { return payload; }
}

View File

@ -0,0 +1,100 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko.telemetry;
import android.content.Context;
import android.os.Build;
import java.io.IOException;
import java.util.Locale;
import com.keepsafe.switchboard.SwitchBoard;
import org.json.JSONArray;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.telemetry.TelemetryConstants.CorePing;
import org.mozilla.gecko.util.StringUtils;
/**
* A class with static methods to generate the various Java-created Telemetry pings to upload to the telemetry server.
*/
public class TelemetryPingGenerator {
// In the server url, the initial path directly after the "scheme://host:port/"
private static final String SERVER_INITIAL_PATH = "submit/telemetry";
/**
* Returns a url of the format:
* http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
*
* @param docId A unique document ID for the ping associated with the upload to this server
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
* @param docType The name of the ping (e.g. "main")
* @return a url at which to POST the telemetry data to
*/
private static String getTelemetryServerURL(final String docId, final String serverURLSchemeHostPort,
final String docType) {
final String appName = AppConstants.MOZ_APP_BASENAME;
final String appVersion = AppConstants.MOZ_APP_VERSION;
final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
final String appBuildId = AppConstants.MOZ_APP_BUILDID;
// The compiler will optimize a single String concatenation into a StringBuilder statement.
// If you change this `return`, be sure to keep it as a single statement to keep it optimized!
return serverURLSchemeHostPort + '/' +
SERVER_INITIAL_PATH + '/' +
docId + '/' +
docType + '/' +
appName + '/' +
appVersion + '/' +
appUpdateChannel + '/' +
appBuildId + '/';
}
/**
* @param docId A unique document ID for the ping associated with the upload to this server
* @param clientId The client ID of this profile (from Gecko)
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
* @throws IOException when client ID could not be created
*/
public static TelemetryPing createCorePing(final Context context, final String docId, final String clientId,
final String serverURLSchemeHostPort, final int seq) {
final String serverURL = getTelemetryServerURL(docId, serverURLSchemeHostPort, CorePing.NAME);
final ExtendedJSONObject payload = createCorePingPayload(context, clientId, seq);
return new TelemetryPing(serverURL, payload);
}
private static ExtendedJSONObject createCorePingPayload(final Context context, final String clientId,
final int seq) {
final ExtendedJSONObject ping = new ExtendedJSONObject();
ping.put(CorePing.VERSION_ATTR, CorePing.VERSION_VALUE);
ping.put(CorePing.OS_ATTR, CorePing.OS_VALUE);
// We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
// manufacturer because we're less likely to have manufacturers with similar names than we are for a
// manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
final String deviceDescriptor =
StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
ping.put(CorePing.ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
ping.put(CorePing.CLIENT_ID, clientId);
ping.put(CorePing.DEVICE, deviceDescriptor);
ping.put(CorePing.LOCALE, Locales.getLanguageTag(Locale.getDefault()));
ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
ping.put(CorePing.SEQ, seq);
if (AppConstants.MOZ_SWITCHBOARD) {
ping.put(CorePing.EXPERIMENTS, getActiveExperiments(context));
}
return ping;
}
private static JSONArray getActiveExperiments(final Context context) {
if (!AppConstants.MOZ_SWITCHBOARD) {
throw new IllegalStateException("This method should not be called with switchboard disabled");
}
return new JSONArray(SwitchBoard.getActiveExperiments(context));
}
}

View File

@ -0,0 +1,220 @@
/* 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/. */
package org.mozilla.gecko.telemetry;
import android.content.Intent;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.util.Log;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.background.BackgroundService;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.util.StringUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
/**
* The service that handles uploading telemetry payloads to the server.
*
* Note that we'll fail to upload if the network is off or background uploads are disabled but the caller is still
* expected to increment the sequence number.
*/
public class TelemetryUploadService extends BackgroundService {
private static final String LOGTAG = StringUtils.safeSubstring("Gecko" + TelemetryUploadService.class.getSimpleName(), 0, 23);
private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
public TelemetryUploadService() {
super(WORKER_THREAD_NAME);
// Intent redelivery can fail hard (e.g. we OOM as we try to upload, the Intent gets redelivered, repeat) so for
// simplicity, we avoid it for now. In the unlikely event that Android kills our upload service, we'll thus fail
// to upload the document with a specific sequence number. Furthermore, we never attempt to re-upload it.
//
// We'll fix this issue in bug 1243585.
setIntentRedelivery(false);
}
/**
* Handles a core ping with the mandatory extras:
* EXTRA_DOC_ID: a unique document ID.
* EXTRA_SEQ: a sequence number for this upload.
* EXTRA_PROFILE_NAME: the gecko profile name.
* EXTRA_PROFILE_PATH: the gecko profile path.
*
* Note that for a given doc ID, seq should always be identical because these are the tools the server uses to
* de-duplicate documents. In order to maintain this consistency, we receive the doc ID and seq from the Intent and
* rely on the caller to update the values. The Service can be killed at any time so we can't ensure seq could be
* incremented properly if we tried to do so in the Service.
*/
@Override
public void onHandleIntent(final Intent intent) {
Log.d(LOGTAG, "Service started");
if (!TelemetryConstants.UPLOAD_ENABLED) {
Log.d(LOGTAG, "Telemetry upload feature is compile-time disabled; not handling upload intent.");
return;
}
if (!isReadyToUpload(intent)) {
return;
}
if (!TelemetryConstants.ACTION_UPLOAD_CORE.equals(intent.getAction())) {
Log.w(LOGTAG, "Unknown action: " + intent.getAction() + ". Returning");
return;
}
final String docId = intent.getStringExtra(TelemetryConstants.EXTRA_DOC_ID);
final int seq = intent.getIntExtra(TelemetryConstants.EXTRA_SEQ, -1);
final String profileName = intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_NAME);
final String profilePath = intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_PATH);
uploadCorePing(docId, seq, profileName, profilePath);
}
private boolean isReadyToUpload(final Intent intent) {
// Intent can be null. Bug 1025937.
if (intent == null) {
Log.d(LOGTAG, "Received null intent. Returning.");
return false;
}
// Don't do anything if the device can't talk to the server.
if (!backgroundDataIsEnabled()) {
Log.d(LOGTAG, "Background data is not enabled; skipping.");
return false;
}
if (intent.getStringExtra(TelemetryConstants.EXTRA_DOC_ID) == null) {
Log.w(LOGTAG, "Received invalid doc ID in Intent. Returning");
return false;
}
if (!intent.hasExtra(TelemetryConstants.EXTRA_SEQ)) {
Log.w(LOGTAG, "Received Intent without sequence number. Returning");
return false;
}
if (intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_NAME) == null) {
Log.w(LOGTAG, "Received invalid profile name in Intent. Returning");
return false;
}
// GeckoProfile can use the name to get the path so this isn't strictly necessary.
// However, getting the path requires parsing an ini file so we optimize by including it here.
if (intent.getStringExtra(TelemetryConstants.EXTRA_PROFILE_PATH) == null) {
Log.w(LOGTAG, "Received invalid profile path in Intent. Returning");
return false;
}
return true;
}
private void uploadCorePing(@NonNull final String docId, final int seq, @NonNull final String profileName,
@NonNull final String profilePath) {
final GeckoProfile profile = GeckoProfile.get(this, profileName, profilePath);
final String clientId;
try {
clientId = profile.getClientId();
} catch (final IOException e) {
// Don't log the exception to avoid leaking the profile path.
Log.w(LOGTAG, "Unable to get client ID to generate core ping: returning.");
return;
}
// Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(this, profileName);
// TODO (bug 1241685): Sync this preference with the gecko preference.
final String serverURLSchemeHostPort =
sharedPrefs.getString(TelemetryConstants.PREF_SERVER_URL, TelemetryConstants.DEFAULT_SERVER_URL);
final TelemetryPing corePing =
TelemetryPingGenerator.createCorePing(this, docId, clientId, serverURLSchemeHostPort, seq);
final CorePingResultDelegate resultDelegate = new CorePingResultDelegate();
uploadPing(corePing, resultDelegate);
}
private void uploadPing(final TelemetryPing ping, final ResultDelegate delegate) {
final BaseResource resource;
try {
resource = new BaseResource(ping.getURL());
} catch (final URISyntaxException e) {
Log.w(LOGTAG, "URISyntaxException for server URL when creating BaseResource: returning.");
return;
}
delegate.setResource(resource);
resource.delegate = delegate;
// We're in a background thread so we don't have any reason to do this asynchronously.
// If we tried, onStartCommand would return and IntentService might stop itself before we finish.
resource.postBlocking(ping.getPayload());
}
private static class CorePingResultDelegate extends ResultDelegate {
public CorePingResultDelegate() {
super();
}
@Override
public String getUserAgent() {
return TelemetryConstants.USER_AGENT;
}
@Override
public void handleHttpResponse(final HttpResponse response) {
final int status = response.getStatusLine().getStatusCode();
switch (status) {
case 200:
case 201:
Log.d(LOGTAG, "Telemetry upload success.");
break;
default:
Log.w(LOGTAG, "Telemetry upload failure. HTTP status: " + status);
}
}
@Override
public void handleHttpProtocolException(final ClientProtocolException e) {
// We don't log the exception to prevent leaking user data.
Log.w(LOGTAG, "HttpProtocolException when trying to upload telemetry");
}
@Override
public void handleHttpIOException(final IOException e) {
// We don't log the exception to prevent leaking user data.
Log.w(LOGTAG, "HttpIOException when trying to upload telemetry");
}
@Override
public void handleTransportException(final GeneralSecurityException e) {
// We don't log the exception to prevent leaking user data.
Log.w(LOGTAG, "Transport exception when trying to upload telemetry");
}
}
/**
* A hack because I want to set the resource after the Delegate is constructed.
* Be sure to call {@link #setResource(Resource)}!
*/
private static abstract class ResultDelegate extends BaseResourceDelegate {
public ResultDelegate() {
super(null);
}
protected void setResource(final Resource resource) {
this.resource = resource;
}
}
}

View File

@ -6,6 +6,7 @@
package org.mozilla.gecko.util;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.mozilla.gecko.AppConstants.Versions;
@ -240,4 +241,10 @@ public class StringUtils {
return Collections.unmodifiableSet(names);
}
public static String safeSubstring(@NonNull final String str, final int start, final int end) {
return str.substring(
Math.max(0, start),
Math.min(end, str.length()));
}
}

View File

@ -540,6 +540,10 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'tabs/TabsPanel.java',
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',
'telemetry/TelemetryConstants.java',
'telemetry/TelemetryPing.java',
'telemetry/TelemetryPingGenerator.java',
'telemetry/TelemetryUploadService.java',
'TelemetryContract.java',
'TextSelection.java',
'TextSelectionHandle.java',

View File

@ -541,6 +541,7 @@
<string name="updater_permission_title">&brandShortName;</string>
<string name="updater_permission_text">&updater_permission_text;</string>
<string name="updater_permission_allow">&updater_permission_allow;</string>
<!-- Awesomescreen screen -->
<string name="suggestions_prompt">&suggestions_prompt3;</string>

View File

@ -497,6 +497,16 @@ public class BaseResource implements Resource {
post(jsonEntity(o));
}
/**
* Perform an HTTP POST as with {@link BaseResource#post(ExtendedJSONObject)}, returning only
* after callbacks have been invoked.
*/
public void postBlocking(final ExtendedJSONObject o) {
// Until we use the asynchronous Apache HttpClient, we can simply call
// through.
post(jsonEntity(o));
}
public void post(JSONObject jsonObject) throws UnsupportedEncodingException {
post(jsonEntity(jsonObject));
}

View File

@ -545,6 +545,8 @@ Statement::Reset()
NS_IMETHODIMP
Statement::BindParameters(mozIStorageBindingParamsArray *aParameters)
{
NS_ENSURE_ARG_POINTER(aParameters);
if (!mDBStatement)
return NS_ERROR_NOT_INITIALIZED;

View File

@ -165,6 +165,16 @@ function test_failed_execute()
stmt.finalize();
}
function test_bind_undefined()
{
var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')");
expectError(Cr.NS_ERROR_ILLEGAL_VALUE,
() => stmt.bindParameters(undefined));
stmt.finalize();
}
var tests = [test_parameterCount_none, test_parameterCount_one,
test_getParameterName, test_getParameterIndex_different,
test_getParameterIndex_same, test_columnCount,
@ -173,6 +183,7 @@ var tests = [test_parameterCount_none, test_parameterCount_one,
test_state_executing, test_state_after_finalize,
test_getColumnDecltype,
test_failed_execute,
test_bind_undefined,
];
function run_test()

View File

@ -50,6 +50,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
Cu.import("resource://gre/modules/ExtensionManagement.jsm");
@ -203,6 +205,8 @@ var Management = {
// extension pages (which run in the chrome process).
var globalBroker = new MessageBroker([Services.mm, Services.ppmm]);
var gContextId = 0;
// An extension page is an execution context for any extension content
// that runs in the chrome process. It's used for background pages
// (type="background"), popups (type="popup"), and any extension
@ -222,6 +226,8 @@ ExtensionPage = function(extension, params) {
this.uri = uri || extension.baseURI;
this.incognito = params.incognito || false;
this.onClose = new Set();
this.contextId = gContextId++;
this.unloaded = false;
// This is the MessageSender property passed to extension.
// It can be augmented by the "page-open" hook.
@ -270,6 +276,17 @@ ExtensionPage.prototype = {
return true;
},
// A wrapper around MessageChannel.sendMessage which adds the extension ID
// to the recipient object, and ensures replies are not processed after the
// context has been unloaded.
sendMessage(target, messageName, data, recipient = {}, sender = {}) {
recipient.extensionId = this.extension.id;
sender.extensionId = this.extension.id;
sender.contextId = this.contextId;
return MessageChannel.sendMessage(target, messageName, data, recipient, sender);
},
callOnClose(obj) {
this.onClose.add(obj);
},
@ -287,6 +304,20 @@ ExtensionPage.prototype = {
// This method is called when an extension page navigates away or
// its tab is closed.
unload() {
// Note that without this guard, we end up running unload code
// multiple times for tab pages closed by the "page-unload" handlers
// triggered below.
if (this.unloaded) {
return;
}
this.unloaded = true;
MessageChannel.abortResponses({
extensionId: this.extension.id,
contextId: this.contextId,
});
Management.emit("page-unload", this);
this.extension.views.delete(this);
@ -1076,6 +1107,8 @@ Extension.prototype = extend(Object.create(ExtensionData.prototype), {
Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id});
MessageChannel.abortResponses({ extensionId: this.id });
ExtensionManagement.shutdownExtension(this.uuid);
// Clean up a generated file.

View File

@ -29,6 +29,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
"resource://gre/modules/MatchPattern.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
"resource://gre/modules/MessageChannel.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
@ -107,12 +111,14 @@ var api = context => {
};
// Represents a content script.
function Script(options) {
function Script(options, deferred = PromiseUtils.defer()) {
this.options = options;
this.run_at = this.options.run_at;
this.js = this.options.js || [];
this.css = this.options.css || [];
this.deferred = deferred;
this.matches_ = new MatchPattern(this.options.matches);
this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
// TODO: MatchPattern should pre-mangle host-only patterns so that we
@ -137,16 +143,6 @@ Script.prototype = {
return false;
}
if ("innerWindowID" in this.options) {
let innerWindowID = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
if (innerWindowID !== this.options.innerWindowID) {
return false;
}
}
// TODO: match_about_blank.
return true;
@ -154,6 +150,7 @@ Script.prototype = {
tryInject(extension, window, sandbox, shouldRun) {
if (!this.matches(window)) {
this.deferred.reject();
return;
}
@ -172,6 +169,7 @@ Script.prototype = {
}
}
let result;
let scheduled = this.run_at || "document_idle";
if (shouldRun(scheduled)) {
for (let url of this.js) {
@ -189,13 +187,26 @@ Script.prototype = {
charset: "UTF-8",
async: AppConstants.platform == "gonk",
};
runSafeSyncWithoutClone(Services.scriptloader.loadSubScriptWithOptions, url, options);
try {
result = Services.scriptloader.loadSubScriptWithOptions(url, options);
} catch (e) {
Cu.reportError(e);
this.deferred.reject(e.message);
}
}
if (this.options.jsCode) {
Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
try {
result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
} catch (e) {
Cu.reportError(e);
this.deferred.reject(e.message);
}
}
}
// TODO: Handle this correctly when we support runAt and allFrames.
this.deferred.resolve(result);
},
};
@ -404,6 +415,8 @@ var DocumentManager = {
} else if (topic == "inner-window-destroyed") {
let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
MessageChannel.abortResponses({ innerWindowID: windowId });
// Close any existent content-script context for the destroyed window.
if (this.contentScriptWindows.has(windowId)) {
let extensions = this.contentScriptWindows.get(windowId);
@ -446,7 +459,7 @@ var DocumentManager = {
let window = global.content;
let context = this.getContentScriptContext(extensionId, window);
if (!context) {
return;
throw new Error("Unexpected add-on ID");
}
// TODO: Somehow make sure we have the right permissions for this origin!
@ -535,6 +548,8 @@ var DocumentManager = {
}
}
MessageChannel.abortResponses({ extensionId });
this.extensionCount--;
if (this.extensionCount == 0) {
this.uninit();
@ -645,44 +660,63 @@ ExtensionManager = {
},
};
class ExtensionGlobal {
constructor(global) {
this.global = global;
MessageChannel.addListener(global, "Extension:Execute", this);
this.broker = new MessageBroker([global]);
this.windowId = global.content
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
global.sendAsyncMessage("Extension:TopWindowID", { windowId: this.windowId });
}
uninit() {
this.global.sendAsyncMessage("Extension:RemoveTopWindowID", { windowId: this.windowId });
}
get messageFilter() {
return {
innerWindowID: this.global.content
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID,
};
}
receiveMessage({ target, messageName, recipient, data }) {
switch (messageName) {
case "Extension:Execute":
let deferred = PromiseUtils.defer();
let script = new Script(data.options, deferred);
let { extensionId } = recipient;
DocumentManager.executeScript(target, extensionId, script);
return deferred.promise;
}
}
}
this.ExtensionContent = {
globals: new Map(),
init(global) {
let broker = new MessageBroker([global]);
this.globals.set(global, broker);
global.addMessageListener("Extension:Execute", this);
let windowId = global.content
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
global.sendAsyncMessage("Extension:TopWindowID", {windowId});
this.globals.set(global, new ExtensionGlobal(global));
},
uninit(global) {
this.globals.get(global).uninit();
this.globals.delete(global);
let windowId = global.content
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
global.sendAsyncMessage("Extension:RemoveTopWindowID", {windowId});
},
getBroker(messageManager) {
return this.globals.get(messageManager);
},
receiveMessage({target, name, data}) {
switch (name) {
case "Extension:Execute":
let script = new Script(data.options);
let {extensionId} = data;
DocumentManager.executeScript(target, extensionId, script);
break;
}
return this.globals.get(messageManager).broker;
},
};

View File

@ -0,0 +1,610 @@
/* 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/. */
"use strict";
/**
* This module provides wrappers around standard message managers to
* simplify bidirectional communication. It currently allows a caller to
* send a message to a single listener, and receive a reply. If there
* are no matching listeners, or the message manager disconnects before
* a reply is received, the caller is returned an error.
*
* Since each message must have only one recipient, the listener end may
* specify filters for the messages it wishes to receive, and the sender
* end likewise may specify recipient tags to match the filters.
*
* The message handler on the listener side may return its response
* value directly, or may return a promise, the resolution or rejection
* of which will be returned instead. The sender end likewise receives a
* promise which resolves or rejects to the listener's response.
*
*
* A basic setup works something like this:
*
* A content script adds a message listener to its global
* nsIContentFrameMessageManager, with an appropriate set of filters:
*
* {
* init(messageManager, window, extensionID) {
* this.window = window;
*
* MessageChannel.addListener(
* messageManager, "ContentScript:TouchContent",
* this);
*
* this.messageFilter = {
* innerWindowID: getInnerWindowID(window),
* extensionID: extensionID,
* };
* },
*
* receiveMessage({ target, messageName, sender, recipient, data }) {
* if (messageName == "ContentScript:TouchContent") {
* return new Promise(resolve => {
* this.touchWindow(data.touchWith, result => {
* resolve({ touchResult: result });
* });
* });
* }
* },
* };
*
* A script in the parent process sends a message to the content process
* via a tab message manager, including recipient tags to match its
* filter, and an optional sender tag to identify itself:
*
* let data = { touchWith: "pencil" };
* let sender = { extensionID, contextID };
* let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID };
*
* MessageChannel.sendMessage(
* tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
* data, recipient, sender
* ).then(result => {
* alert(result.touchResult);
* });
*
* Since the lifetimes of message senders and receivers may not always
* match, either side of the message channel may cancel pending
* responses which match its sender or recipient tags.
*
* For the above client, this might be done from an
* inner-window-destroyed observer, when its target scope is destroyed:
*
* observe(subject, topic, data) {
* if (topic == "inner-window-destroyed") {
* let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
*
* MessageChannel.abortResponses({ innerWindowID });
* }
* },
*
* From the parent, it may be done when its context is being destroyed:
*
* onDestroy() {
* MessageChannel.abortResponses({
* extensionID: this.extensionID,
* contextID: this.contextID,
* });
* },
*
*/
this.EXPORTED_SYMBOLS = ["MessageChannel"];
/* globals MessageChannel */
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
/**
* Handles the mapping and dispatching of messages to their registered
* handlers. There is one broker per message manager and class of
* messages. Each class of messages is mapped to one native message
* name, e.g., "MessageChannel:Message", and is dispatched to handlers
* based on an internal message name, e.g., "Extension:ExecuteScript".
*/
class FilteringMessageManager {
/**
* @param {string} messageName
* The name of the native message this broker listens for.
* @param {function} callback
* A function which is called for each message after it has been
* mapped to its handler. The function receives two arguments:
*
* result:
* An object containing either a `handler` or an `error` property.
* If no error occurs, `handler` will be a matching handler that
* was registered by `addHandler`. Otherwise, the `error` property
* will contain an object describing the error.
*
* data:
* An object describing the message, as defined in
* `MessageChannel.addListener`.
*/
constructor(messageName, callback, messageManager) {
this.messageName = messageName;
this.callback = callback;
this.messageManager = messageManager;
this.messageManager.addMessageListener(this.messageName, this);
this.handlers = new Map();
}
/**
* Receives a message from our message manager, maps it to a handler, and
* passes the result to our message callback.
*/
receiveMessage({ data, target }) {
let handlers = Array.from(this.getHandlers(data.messageName, data.recipient));
let result = {};
if (handlers.length == 0) {
result.error = { result: MessageChannel.RESULT_NO_HANDLER,
message: "No matching message handler" };
} else if (handlers.length > 1) {
result.error = { result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
message: `Multiple matching handlers for ${data.messageName}` };
} else {
result.handler = handlers[0];
}
data.target = target;
this.callback(result, data);
}
/**
* Iterates over all handlers for the given message name. If `recipient`
* is provided, only iterates over handlers whose filters match it.
*
* @param {string|number} messageName
* The message for which to return handlers.
* @param {object} recipient
* The recipient data on which to filter handlers.
*/
* getHandlers(messageName, recipient) {
let handlers = this.handlers.get(messageName) || new Set();
for (let handler of handlers) {
if (MessageChannel.matchesFilter(handler.messageFilter, recipient)) {
yield handler;
}
}
}
/**
* Registers a handler for the given message.
*
* @param {string} messageName
* The internal message name for which to register the handler.
* @param {object} handler
* An opaque handler object. The object must have a `messageFilter`
* property on which to filter messages. Final dispatching is handled
* by the message callback passed to the constructor.
*/
addHandler(messageName, handler) {
if (!this.handlers.has(messageName)) {
this.handlers.set(messageName, new Set());
}
this.handlers.get(messageName).add(handler);
}
/**
* Unregisters a handler for the given message.
*
* @param {string} messageName
* The internal message name for which to unregister the handler.
* @param {object} handler
* The handler object to unregister.
*/
removeHandler(messageName, handler) {
this.handlers.get(messageName).delete(handler);
}
}
/**
* Manages mappings of message managers to their corresponding message
* brokers. Brokers are lazily created for each message manager the
* first time they are accessed. In the case of content frame message
* managers, they are also automatically destroyed when the frame
* unload event fires.
*/
class FilteringMessageManagerMap extends Map {
// Unfortunately, we can't use a WeakMap for this, because message
// managers do not support preserved wrappers.
/**
* @param {string} messageName
* The native message name passed to `FilteringMessageManager` constructors.
* @param {function} callback
* The message callback function passed to
* `FilteringMessageManager` constructors.
*/
constructor(messageName, callback) {
super();
this.messageName = messageName;
this.callback = callback;
}
/**
* Returns, and possibly creates, a message broker for the given
* message manager.
*
* @param {nsIMessageSender} target
* The message manager for which to return a broker.
*
* @returns {FilteringMessageManager}
*/
get(target) {
if (this.has(target)) {
return super.get(target);
}
let broker = new FilteringMessageManager(this.messageName, this.callback, target);
this.set(target, broker);
if (target instanceof Ci.nsIDOMEventTarget) {
let onUnload = event => {
target.removeEventListener("unload", onUnload);
this.delete(target);
};
target.addEventListener("unload", onUnload);
}
return broker;
}
}
const MESSAGE_MESSAGE = "MessageChannel:Message";
const MESSAGE_RESPONSE = "MessageChannel:Response";
let gChannelId = 0;
this.MessageChannel = {
init() {
Services.obs.addObserver(this, "message-manager-close", false);
Services.obs.addObserver(this, "message-manager-disconnect", false);
this.messageManagers = new FilteringMessageManagerMap(
MESSAGE_MESSAGE, this._handleMessage.bind(this));
this.responseManagers = new FilteringMessageManagerMap(
MESSAGE_RESPONSE, this._handleResponse.bind(this));
/**
* Contains a list of pending responses, either waiting to be
* received or waiting to be sent. @see _addPendingResponse
*/
this.pendingResponses = new Set();
},
RESULT_SUCCESS: 0,
RESULT_DISCONNECTED: 1,
RESULT_NO_HANDLER: 2,
RESULT_MULTIPLE_HANDLERS: 3,
RESULT_ERROR: 4,
REASON_DISCONNECTED: {
result: this.RESULT_DISCONNECTED,
message: "Message manager disconnected",
},
/**
* Returns true if the given `data` object matches the given `filter`
* object. The objects match if every property of `filter` is present
* in `data`, and the values in both objects are strictly equal.
*
* @param {object} filter
* The filter object to match against.
* @param {object} data
* The data object being matched.
* @returns {bool} True if the objects match.
*/
matchesFilter(filter, data) {
return Object.keys(filter).every(key => {
return key in data && data[key] === filter[key];
});
},
/**
* Adds a message listener to the given message manager.
*
* @param {nsIMessageSender} target
* The message manager on which to listen.
* @param {string|number} messageName
* The name of the message to listen for.
* @param {MessageReceiver} handler
* The handler to dispatch to. Must be an object with the following
* properties:
*
* receiveMessage:
* A method which is called for each message received by the
* listener. The method takes one argument, an object, with the
* following properties:
*
* messageName:
* The internal message name, as passed to `sendMessage`.
*
* target:
* The message manager which received this message.
*
* channelId:
* The internal ID of the transaction, used to map responses to
* the original sender.
*
* sender:
* An object describing the sender, as passed to `sendMessage`.
*
* recipient:
* An object describing the recipient, as passed to
* `sendMessage`.
*
* data:
* The contents of the message, as passed to `sendMessage`.
*
* The method may return any structured-clone-compatible
* object, which will be returned as a response to the message
* sender. It may also instead return a `Promise`, the
* resolution or rejection value of which will likewise be
* returned to the message sender.
*
* messageFilter:
* An object containing arbitrary properties on which to filter
* received messages. Messages will only be dispatched to this
* object if the `recipient` object passed to `sendMessage`
* matches this filter, as determined by `matchesFilter`.
*/
addListener(target, messageName, handler) {
this.messageManagers.get(target).addHandler(messageName, handler);
},
/**
* Removes a message listener from the given message manager.
*
* @param {nsIMessageSender} target
* The message manager on which to stop listening.
* @param {string|number} messageName
* The name of the message to stop listening for.
* @param {MessageReceiver} handler
* The handler to stop dispatching to.
*/
removeListener(target, messageName, handler) {
this.messageManagers.get(target).removeListener(messageName, handler);
},
/**
* Sends a message via the given message manager. Returns a promise which
* resolves or rejects with the return value of the message receiver.
*
* The promise also rejects if there is no matching listener, or the other
* side of the message manager disconnects before the response is received.
*
* @param {nsIMessageSender} target
* The message manager on which to send the message.
* @param {string} messageName
* The name of the message to send, as passed to `addListener`.
* @param {object} data
* A structured-clone-compatible object to send to the message
* recipient.
* @param {object} [recipient]
* A structured-clone-compatible object to identify the message
* recipient. The object must match the `messageFilter` defined by
* recipients in order for the message to be received.
* @param {object} [sender]
* A structured-clone-compatible object to identify the message
* sender. This object may also be used as a filter to prematurely
* abort responses when the sender is being destroyed.
* @see `abortResponses`.
* @returns Promise
*/
sendMessage(target, messageName, data, recipient = {}, sender = {}) {
let channelId = gChannelId++;
let message = { messageName, channelId, sender, recipient, data };
let deferred = PromiseUtils.defer();
deferred.messageFilter = {};
deferred.sender = recipient;
deferred.messageManager = target;
this._addPendingResponse(deferred);
// The channel ID is used as the message name when routing responses.
// Add a message listener to the response broker, and remove it once
// we've gotten (or canceled) a response.
let broker = this.responseManagers.get(target);
broker.addHandler(channelId, deferred);
let cleanup = () => {
broker.removeHandler(channelId, deferred);
};
deferred.promise.then(cleanup, cleanup);
target.sendAsyncMessage(MESSAGE_MESSAGE, message);
return deferred.promise;
},
/**
* Handles dispatching message callbacks from the message brokers to their
* appropriate `MessageReceivers`, and routing the responses back to the
* original senders.
*
* Each handler object is a `MessageReceiver` object as passed to
* `addListener`.
*/
_handleMessage({ handler, error }, data) {
// The target passed to `receiveMessage` is sometimes a message manager
// owner instead of a message manager, so make sure to convert it to a
// message manager first if necessary.
let { target } = data;
if (!(target instanceof Ci.nsIMessageSender)) {
target = target.messageManager;
}
let deferred = {
sender: data.sender,
messageManager: target,
};
deferred.promise = new Promise((resolve, reject) => {
deferred.reject = reject;
if (handler) {
let result = handler.receiveMessage(data);
resolve(result);
} else {
reject(error);
}
}).then(
value => {
let response = {
result: this.RESULT_SUCCESS,
messageName: data.channelId,
recipient: {},
value,
};
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
},
error => {
let response = {
result: this.RESULT_ERROR,
messageName: data.channelId,
recipient: {},
error,
};
if (error && typeof(error) == "object") {
if (error.result) {
response.result = error.result;
}
}
target.sendAsyncMessage(MESSAGE_RESPONSE, response);
});
this._addPendingResponse(deferred);
},
/**
* Handles message callbacks from the response brokers.
*
* Each handler object is a deferred object created by `sendMessage`, and
* should be resolved or rejected based on the contents of the response.
*/
_handleResponse({ handler, error }, data) {
if (error) {
// If we have an error at this point, we have handler to report it to,
// so just log it.
Cu.reportError(error.message);
} else if (data.result === this.RESULT_SUCCESS) {
handler.resolve(data.value);
} else {
handler.reject(data);
}
},
/**
* Adds a pending response to the the `pendingResponses` list.
*
* The response object must be a deferred promise with the following
* properties:
*
* promise:
* The promise object which resolves or rejects when the response
* is no longer pending.
*
* reject:
* A function which, when called, causes the `promise` object to be
* rejected.
*
* sender:
* A sender object, as passed to `sendMessage.
*
* messageManager:
* The message manager the response will be sent or received on.
*
* When the promise resolves or rejects, it will be removed from the
* list.
*
* These values are used to clear pending responses when execution
* contexts are destroyed.
*/
_addPendingResponse(deferred) {
let cleanup = () => {
this.pendingResponses.delete(deferred);
};
this.pendingResponses.add(deferred);
deferred.promise.then(cleanup, cleanup);
},
/**
* Aborts any pending message responses to senders matching the given
* filter.
*
* @param {object} sender
* The object on which to filter senders, as determined by
* `matchesFilter`.
* @param {object} [reason]
* An optional object describing the reason the response was aborted.
* Will be passed to the promise rejection handler of all aborted
* responses.
*/
abortResponses(sender, reason = this.REASON_DISCONNECTED) {
for (let response of this.pendingResponses) {
if (this.matchesFilter(sender, response.sender)) {
response.reject(reason);
}
}
},
/**
* Aborts any pending message responses to the broker for the given
* message manager.
*
* @param {nsIMessageSender} target
* The message manager for which to abort brokers.
* @param {object} reason
* An object describing the reason the responses were aborted.
* Will be passed to the promise rejection handler of all aborted
* responses.
*/
abortMessageManager(target, reason) {
for (let response of this.pendingResponses) {
if (response.messageManager === target) {
response.reject(reason);
}
}
},
observe(subject, topic, data) {
switch (topic) {
case "message-manager-close":
case "message-manager-disconnect":
try {
if (this.responseManagers.has(subject)) {
this.abortMessageManager(subject, this.REASON_DISCONNECTED);
}
} finally {
this.responseManagers.delete(subject);
this.messageManagers.delete(subject);
}
break;
}
},
};
MessageChannel.init();

View File

@ -10,6 +10,7 @@ EXTRA_JS_MODULES += [
'ExtensionManagement.jsm',
'ExtensionStorage.jsm',
'ExtensionUtils.jsm',
'MessageChannel.jsm',
'Schemas.jsm',
]

View File

@ -0,0 +1,50 @@
"core" ping
============
This mobile-specific ping is intended to provide the most critical
data in a concise format, allowing for frequent uploads.
Since this ping is used to measure retention, it should be sent
each time the browser is opened.
Submission will be per the Edge server specification::
/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
* ``docId`` is a UUID for deduping
* ``docType`` is “core”
* ``appName`` is “Fennec”
* ``appVersion`` is the version of the application (e.g. "46.0a1")
* ``appUpdateChannel`` is “release”, “beta”, etc.
* ``appBuildID`` is the build number
Note: Counts below (e.g. search & usage times) are “since the last
ping”, not total for the whole application lifetime.
Structure::
{
"v": 1, // ping format version
"clientId": <string>, // client id, e.g.
// "c641eacf-c30c-4171-b403-f077724e848a"
"seq": <positive integer>, // running ping counter, e.g. 3
"locale": <string>, // application locale, e.g. "en-US"
"os": <string>, // OS name.
"osversion": <string>, // OS version.
"device": <string>, // Build.MANUFACTURER + " - " + Build.MODEL
// where manufacturer is truncated to 12 characters
// & model is truncated to 19 characters
"arch": <string>, // e.g. "arm", "x86"
"experiments": [<string>, …], // Optional, array of identifiers
// for the active experiments
}
The ``device`` field is filled in with information specified by the hardware
manufacturer. As such, it could be excessively long and use excessive amounts
of limited user data. To avoid this, we limit the length of the field. We're
more likely have collisions for models within a manufacturer (e.g. "Galaxy S5"
vs. "Galaxy Note") than we are for shortened manufacturer names so we provide
more characters for the model than the manufacturer.

View File

@ -19,6 +19,7 @@ Client-side, this consists of:
common-ping
environment
main-ping
core-ping
deletion-ping
crash-ping
uitour-ping

View File

@ -152,6 +152,13 @@ IMEHandler::ProcessMessage(nsWindow* aWindow, UINT aMessage,
WPARAM& aWParam, LPARAM& aLParam,
MSGResult& aResult)
{
if (aMessage == MOZ_WM_DISMISS_ONSCREEN_KEYBOARD) {
if (!sFocusedWindow) {
DismissOnScreenKeyboard();
}
return true;
}
#ifdef NS_ENABLE_TSF
if (IsTSFAvailable()) {
TSFTextStore::ProcessMessage(aWindow, aMessage, aWParam, aLParam, aResult);
@ -253,7 +260,7 @@ IMEHandler::NotifyIME(nsWindow* aWindow,
}
case NOTIFY_IME_OF_BLUR:
sFocusedWindow = nullptr;
IMEHandler::MaybeDismissOnScreenKeyboard();
IMEHandler::MaybeDismissOnScreenKeyboard(aWindow);
IMMHandler::OnFocusChange(false, aWindow);
return TSFTextStore::OnFocusChange(false, aWindow,
aWindow->GetInputContext());
@ -302,11 +309,13 @@ IMEHandler::NotifyIME(nsWindow* aWindow,
case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT:
return IMMHandler::OnMouseButtonEvent(aWindow, aIMENotification);
case NOTIFY_IME_OF_FOCUS:
sFocusedWindow = aWindow;
IMMHandler::OnFocusChange(true, aWindow);
IMEHandler::MaybeShowOnScreenKeyboard();
return NS_OK;
case NOTIFY_IME_OF_BLUR:
IMEHandler::MaybeDismissOnScreenKeyboard();
sFocusedWindow = nullptr;
IMEHandler::MaybeDismissOnScreenKeyboard(aWindow);
IMMHandler::OnFocusChange(false, aWindow);
#ifdef NS_ENABLE_TSF
// If a plugin gets focus while TSF has focus, we need to notify TSF of
@ -597,14 +606,15 @@ IMEHandler::MaybeShowOnScreenKeyboard()
// static
void
IMEHandler::MaybeDismissOnScreenKeyboard()
IMEHandler::MaybeDismissOnScreenKeyboard(nsWindow* aWindow)
{
if (sPluginHasFocus ||
!IsWin8OrLater()) {
return;
}
IMEHandler::DismissOnScreenKeyboard();
::PostMessage(aWindow->GetWindowHandle(), MOZ_WM_DISMISS_ONSCREEN_KEYBOARD,
0, 0);
}
// static

View File

@ -141,7 +141,7 @@ private:
static bool IsIMMActive();
static void MaybeShowOnScreenKeyboard();
static void MaybeDismissOnScreenKeyboard();
static void MaybeDismissOnScreenKeyboard(nsWindow* aWindow);
static bool WStringStartsWithCaseInsensitive(const std::wstring& aHaystack,
const std::wstring& aNeedle);
static bool IsKeyboardPresentOnSlate();

View File

@ -41,6 +41,8 @@
#define MOZ_WM_NOTIY_TSF_OF_LAYOUT_CHANGE (WM_APP+0x0315)
// Internal message used in correcting backwards clock skew
#define MOZ_WM_SKEWFIX (WM_APP+0x0316)
// Internal message used for hiding the on-screen keyboard
#define MOZ_WM_DISMISS_ONSCREEN_KEYBOARD (WM_APP+0x0317)
// Internal message for ensuring the file picker is visible on multi monitor
// systems, and when the screen resolution changes.