diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js
index 7a77f5ee0b8..3ff29b14869 100644
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -847,3 +847,6 @@ pref("media.webspeech.synth.enabled", true);
// Downloads API
pref("dom.mozDownloads.enabled", true);
pref("dom.downloads.max_retention_days", 7);
+
+// The URL of the Firefox Accounts auth server backend
+pref("identity.fxaccounts.auth.uri", "https://api-accounts.dev.lcip.org/v1");
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 7a41080ae7c..d1613722b15 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1335,3 +1335,6 @@ pref("network.disable.ipc.security", true);
// CustomizableUI debug logging.
pref("browser.uiCustomization.debug", false);
+
+// The URL of the Firefox Accounts auth server backend
+pref("identity.fxaccounts.auth.uri", "https://api-accounts.dev.lcip.org/v1");
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index f7ea2eb3743..b98724dd11e 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -123,13 +123,14 @@ tabbrowser {
/* Explicitly set the visibility to override the value (collapsed)
* we inherit from #TabsToolbar[collapsed] upon opening a browser window. */
visibility: visible;
- /* This transition is only applied when opening a new tab. Closing tabs
- * are just hidden so we don't need to adjust the delay for that. */
+ /* The transition is only delayed for opening tabs. */
transition: visibility 0ms 25ms;
}
-.tab-background[selected]:not([fadein]):not([pinned]) {
+.tab-background:not([fadein]):not([pinned]) {
visibility: hidden;
+ /* Closing tabs are hidden without a delay. */
+ transition-delay: 0ms;
}
.tab-throbber:not([fadein]):not([pinned]),
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 9e95ce2f75b..7e35064bd22 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4163,6 +4163,7 @@ function onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
var firstMenuItem = aInsertPoint || popup.firstChild;
let toolbarNodes = Array.slice(gNavToolbox.childNodes);
+ toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars);
for (let toolbar of toolbarNodes) {
let toolbarName = toolbar.getAttribute("toolbarname");
diff --git a/browser/components/customizableui/src/CustomizableUI.jsm b/browser/components/customizableui/src/CustomizableUI.jsm
index 916e4348e61..ca49fa39ee2 100644
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -288,11 +288,11 @@ let CustomizableUIInternal = {
}
},
- unregisterArea: function(aName) {
+ unregisterArea: function(aName, aDestroyPlacements) {
if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
throw new Error("Invalid area name");
}
- if (!gAreas.has(aName)) {
+ if (!gAreas.has(aName) && !gPlacements.has(aName)) {
throw new Error("Area not registered");
}
@@ -300,11 +300,22 @@ let CustomizableUIInternal = {
this.beginBatchUpdate();
try {
let placements = gPlacements.get(aName);
- placements.forEach(this.removeWidgetFromArea, this);
+ if (placements) {
+ // Need to clone this array so removeWidgetFromArea doesn't modify it
+ placements = [...placements];
+ placements.forEach(this.removeWidgetFromArea, this);
+ }
// Delete all remaining traces.
gAreas.delete(aName);
- gPlacements.delete(aName);
+ // Only destroy placements when necessary:
+ if (aDestroyPlacements) {
+ gPlacements.delete(aName);
+ } else {
+ // Otherwise we need to re-set them, as removeFromArea will have emptied
+ // them out:
+ gPlacements.set(aName, placements);
+ }
gFuturePlacements.delete(aName);
gBuildAreas.delete(aName);
} finally {
@@ -1206,12 +1217,15 @@ let CustomizableUIInternal = {
return [...widgets];
},
- getPlacementOfWidget: function(aWidgetId, aOnlyRegistered) {
+ getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) {
if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
return null;
}
for (let [area, placements] of gPlacements) {
+ if (!gAreas.has(area) && !aDeadAreas) {
+ continue;
+ }
let index = placements.indexOf(aWidgetId);
if (index != -1) {
return { area: area, position: index };
@@ -1256,7 +1270,7 @@ let CustomizableUIInternal = {
aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
}
- let oldPlacement = this.getPlacementOfWidget(aWidgetId);
+ let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
if (oldPlacement && oldPlacement.area == aArea) {
this.moveWidgetWithinArea(aWidgetId, aPosition);
return;
@@ -1304,7 +1318,7 @@ let CustomizableUIInternal = {
},
removeWidgetFromArea: function(aWidgetId) {
- let oldPlacement = this.getPlacementOfWidget(aWidgetId);
+ let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
if (!oldPlacement) {
return;
}
@@ -2045,8 +2059,8 @@ this.CustomizableUI = {
registerMenuPanel: function(aPanel) {
CustomizableUIInternal.registerMenuPanel(aPanel);
},
- unregisterArea: function(aName) {
- CustomizableUIInternal.unregisterArea(aName);
+ unregisterArea: function(aName, aDestroyPlacements) {
+ CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
},
addWidgetToArea: function(aWidgetId, aArea, aPosition) {
CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
@@ -2236,7 +2250,11 @@ function WidgetGroupWrapper(aWidget) {
return [];
}
let area = placement.area;
- return [this.forWindow(node.ownerDocument.defaultView) for (node of gBuildAreas.get(area))];
+ let buildAreas = gBuildAreas.get(area);
+ if (!buildAreas) {
+ return [];
+ }
+ return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)];
});
this.__defineGetter__("areaType", function() {
diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini
index 2bcc13f93c2..6ab40f2c040 100644
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -40,4 +40,5 @@ skip-if = os == "mac"
[browser_938995_indefaultstate_nonremovable.js]
[browser_940946_removable_from_navbar_customizemode.js]
[browser_941083_invalidate_wrapper_cache_createWidget.js]
+[browser_942581_unregisterArea_keeps_placements.js]
[browser_panel_toggle.js]
diff --git a/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js b/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js
new file mode 100644
index 00000000000..feda11a3252
--- /dev/null
+++ b/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js
@@ -0,0 +1,118 @@
+/* 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/. */
+
+const kToolbarName = "test-unregisterArea-placements-toolbar";
+const kTestWidgetPfx = "test-widget-for-unregisterArea-placements-";
+const kTestWidgetCount = 3;
+
+let gTests = [
+ {
+ desc: "unregisterArea should keep placements by default and restore them when re-adding the area",
+ run: function() {
+ let widgetIds = []
+ for (let i = 0; i < kTestWidgetCount; i++) {
+ let id = kTestWidgetPfx + i;
+ widgetIds.push(id);
+ let spec = {id: id, type: 'button', removable: true, label: "unregisterArea test", tooltiptext: "" + i};
+ CustomizableUI.createWidget(spec);
+ }
+ for (let i = kTestWidgetCount; i < kTestWidgetCount * 2; i++) {
+ let id = kTestWidgetPfx + i;
+ widgetIds.push(id);
+ createDummyXULButton(id, "unregisterArea XUL test " + i);
+ }
+ let toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds);
+ checkAbstractAndRealPlacements(toolbarNode, widgetIds);
+
+ // Now move one of them:
+ CustomizableUI.moveWidgetWithinArea(kTestWidgetPfx + kTestWidgetCount, 0);
+ // Clone the array so we know this is the modified one:
+ let modifiedWidgetIds = [...widgetIds];
+ let movedWidget = modifiedWidgetIds.splice(kTestWidgetCount, 1)[0];
+ modifiedWidgetIds.unshift(movedWidget);
+
+ // Check it:
+ checkAbstractAndRealPlacements(toolbarNode, modifiedWidgetIds);
+
+ // Then unregister
+ CustomizableUI.unregisterArea(kToolbarName);
+
+ // Check we tell the outside world no dangerous things:
+ checkWidgetFates(widgetIds);
+ // Only then remove the real node
+ toolbarNode.remove();
+
+ // Now move one of the items to the palette, and another to the navbar:
+ let lastWidget = modifiedWidgetIds.pop();
+ CustomizableUI.removeWidgetFromArea(lastWidget);
+ lastWidget = modifiedWidgetIds.pop();
+ CustomizableUI.addWidgetToArea(lastWidget, CustomizableUI.AREA_NAVBAR);
+
+ // Recreate ourselves with the default placements being the same:
+ toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds);
+ // Then check that after doing this, our actual placements match
+ // the modified list, not the default one.
+ checkAbstractAndRealPlacements(toolbarNode, modifiedWidgetIds);
+
+ // Now remove completely:
+ CustomizableUI.unregisterArea(kToolbarName, true);
+ checkWidgetFates(modifiedWidgetIds);
+ toolbarNode.remove();
+
+ // One more time:
+ // Recreate ourselves with the default placements being the same:
+ toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds);
+ // Should now be back to default:
+ checkAbstractAndRealPlacements(toolbarNode, widgetIds);
+ CustomizableUI.unregisterArea(kToolbarName, true);
+ checkWidgetFates(widgetIds);
+ toolbarNode.remove();
+
+ //XXXgijs: ensure cleanup function doesn't barf:
+ gAddedToolbars.delete(kToolbarName);
+
+ // Remove all the XUL widgets, destroy the others:
+ for (let widget of widgetIds) {
+ let widgetWrapper = CustomizableUI.getWidget(widget);
+ if (widgetWrapper.provider == CustomizableUI.PROVIDER_XUL) {
+ gNavToolbox.palette.querySelector("#" + widget).remove();
+ } else {
+ CustomizableUI.destroyWidget(widget);
+ }
+ }
+ },
+ }
+];
+
+function checkAbstractAndRealPlacements(aNode, aExpectedPlacements) {
+ assertAreaPlacements(kToolbarName, aExpectedPlacements);
+ let physicalWidgetIds = [node.id for (node of aNode.childNodes)];
+ placementArraysEqual(aNode.id, physicalWidgetIds, aExpectedPlacements);
+}
+
+function checkWidgetFates(aWidgetIds) {
+ for (let widget of aWidgetIds) {
+ ok(!CustomizableUI.getPlacementOfWidget(widget), "Widget should be in palette");
+ ok(!document.getElementById(widget), "Widget should not be in the DOM");
+ let widgetInPalette = !!gNavToolbox.palette.querySelector("#" + widget);
+ let widgetProvider = CustomizableUI.getWidget(widget).provider;
+ let widgetIsXULWidget = widgetProvider == CustomizableUI.PROVIDER_XUL;
+ is(widgetInPalette, widgetIsXULWidget, "Just XUL Widgets should be in the palette");
+ }
+}
+
+function asyncCleanup() {
+ yield resetCustomization();
+}
+
+function cleanup() {
+ removeCustomToolbars();
+}
+
+function test() {
+ waitForExplicitFinish();
+ registerCleanupFunction(cleanup);
+ runTests(gTests, asyncCleanup);
+}
+
diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js
index 5e1b7ba8e8e..1de61e07fc2 100644
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -39,12 +39,13 @@ function createToolbarWithPlacements(id, placements) {
defaultPlacements: placements
});
gNavToolbox.appendChild(tb);
+ return tb;
}
function removeCustomToolbars() {
CustomizableUI.reset();
for (let toolbarId of gAddedToolbars) {
- CustomizableUI.unregisterArea(toolbarId);
+ CustomizableUI.unregisterArea(toolbarId, true);
document.getElementById(toolbarId).remove();
}
gAddedToolbars.clear();
@@ -71,6 +72,10 @@ function addSwitchToMetroButtonInWindows8(areaPanelPlacements) {
function assertAreaPlacements(areaId, expectedPlacements) {
let actualPlacements = getAreaWidgetIds(areaId);
+ placementArraysEqual(areaId, actualPlacements, expectedPlacements);
+}
+
+function placementArraysEqual(areaId, actualPlacements, expectedPlacements) {
is(actualPlacements.length, expectedPlacements.length,
"Area " + areaId + " should have " + expectedPlacements.length + " items.");
let minItems = Math.min(expectedPlacements.length, actualPlacements.length);
diff --git a/browser/devtools/profiler/cleopatra.js b/browser/devtools/profiler/cleopatra.js
index 4da6335602b..fa065255cd7 100644
--- a/browser/devtools/profiler/cleopatra.js
+++ b/browser/devtools/profiler/cleopatra.js
@@ -4,7 +4,8 @@
"use strict";
-let { defer } = require("sdk/core/promise");
+let { Cu } = require("chrome");
+let { defer } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
let EventEmitter = require("devtools/shared/event-emitter");
const { PROFILE_IDLE, PROFILE_COMPLETED, PROFILE_RUNNING } = require("devtools/profiler/consts");
diff --git a/browser/devtools/profiler/commands.js b/browser/devtools/profiler/commands.js
index ede2d906131..2fc2389db0c 100644
--- a/browser/devtools/profiler/commands.js
+++ b/browser/devtools/profiler/commands.js
@@ -10,7 +10,7 @@ Cu.import("resource://gre/modules/devtools/gcli.jsm");
loader.lazyGetter(this, "gDevTools",
() => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
-var promise = require("sdk/core/promise");
+var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
/*
* 'profiler' command. Doesn't do anything.
diff --git a/browser/devtools/profiler/panel.js b/browser/devtools/profiler/panel.js
index fd7a3834677..498447c8f6c 100644
--- a/browser/devtools/profiler/panel.js
+++ b/browser/devtools/profiler/panel.js
@@ -17,10 +17,10 @@ const {
const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
var EventEmitter = require("devtools/shared/event-emitter");
-var promise = require("sdk/core/promise");
var Cleopatra = require("devtools/profiler/cleopatra");
var Sidebar = require("devtools/profiler/sidebar");
var ProfilerController = require("devtools/profiler/controller");
+var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
Cu.import("resource:///modules/devtools/gDevTools.jsm");
Cu.import("resource://gre/modules/devtools/Console.jsm");
diff --git a/browser/devtools/scratchpad/scratchpad-panel.js b/browser/devtools/scratchpad/scratchpad-panel.js
index cf351aba2fd..293f02307cd 100644
--- a/browser/devtools/scratchpad/scratchpad-panel.js
+++ b/browser/devtools/scratchpad/scratchpad-panel.js
@@ -5,8 +5,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
+const {Cu} = require("chrome");
const EventEmitter = require("devtools/shared/event-emitter");
-const promise = require("sdk/core/promise");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
function ScratchpadPanel(iframeWindow, toolbox) {
@@ -14,7 +15,7 @@ function ScratchpadPanel(iframeWindow, toolbox) {
this._toolbox = toolbox;
this.panelWin = iframeWindow;
this.scratchpad = Scratchpad;
-
+
Scratchpad.target = this.target;
Scratchpad.hideMenu();
diff --git a/browser/devtools/scratchpad/scratchpad.js b/browser/devtools/scratchpad/scratchpad.js
index 92c2035deef..278071c9d34 100644
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -33,11 +33,12 @@ const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-const promise = require("sdk/core/promise");
+
const Telemetry = require("devtools/shared/telemetry");
const Editor = require("devtools/sourceeditor/editor");
const TargetFactory = require("devtools/framework/target").TargetFactory;
+const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
diff --git a/browser/devtools/scratchpad/test/head.js b/browser/devtools/scratchpad/test/head.js
index dc2cf5e15ce..de820bfd9fa 100644
--- a/browser/devtools/scratchpad/test/head.js
+++ b/browser/devtools/scratchpad/test/head.js
@@ -4,16 +4,9 @@
"use strict";
-let tempScope = {};
-
-Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
-Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
-Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", tempScope);
-
-
-let NetUtil = tempScope.NetUtil;
-let FileUtils = tempScope.FileUtils;
-let promise = tempScope.Promise;
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
let gScratchpadWindow; // Reference to the Scratchpad chrome window object
diff --git a/browser/devtools/sourceeditor/editor.js b/browser/devtools/sourceeditor/editor.js
index 626898cf05d..a603110ded8 100644
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -16,7 +16,7 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.x
// while shifting to a line which was initially out of view.
const MAX_VERTICAL_OFFSET = 3;
-const promise = require("sdk/core/promise");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const events = require("devtools/shared/event-emitter");
Cu.import("resource://gre/modules/Services.jsm");
diff --git a/browser/metro/base/content/browser.js b/browser/metro/base/content/browser.js
index 03846ac326a..658c3529e56 100644
--- a/browser/metro/base/content/browser.js
+++ b/browser/metro/base/content/browser.js
@@ -1308,7 +1308,6 @@ Tab.prototype = {
Elements.browsers.addEventListener("SizeChanged", this, false);
browser.messageManager.addMessageListener("Content:StateChange", this);
- Services.obs.addObserver(this, "metro_viewstate_changed", false);
if (aOwner)
this._copyHistoryFrom(aOwner);
@@ -1323,8 +1322,11 @@ Tab.prototype = {
handleEvent: function (aEvent) {
switch (aEvent.type) {
case "DOMWindowCreated":
+ this.updateViewport();
+ break;
case "SizeChanged":
this.updateViewport();
+ this._delayUpdateThumbnail();
break;
}
},
@@ -1336,30 +1338,23 @@ Tab.prototype = {
this.updateThumbnail();
// ...and in a little while to capture page after load.
if (aMessage.json.stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
- clearTimeout(this._updateThumbnailTimeout);
- this._updateThumbnailTimeout = setTimeout(() => {
- this.updateThumbnail();
- }, kTabThumbnailDelayCapture);
+ this._delayUpdateThumbnail();
}
break;
}
},
- observe: function BrowserUI_observe(aSubject, aTopic, aData) {
- switch (aTopic) {
- case "metro_viewstate_changed":
- if (aData !== "snapped") {
- this.updateThumbnail();
- }
- break;
- }
+ _delayUpdateThumbnail: function() {
+ clearTimeout(this._updateThumbnailTimeout);
+ this._updateThumbnailTimeout = setTimeout(() => {
+ this.updateThumbnail();
+ }, kTabThumbnailDelayCapture);
},
destroy: function destroy() {
this._browser.messageManager.removeMessageListener("Content:StateChange", this);
this._browser.removeEventListener("DOMWindowCreated", this, false);
Elements.browsers.removeEventListener("SizeChanged", this, false);
- Services.obs.removeObserver(this, "metro_viewstate_changed", false);
clearTimeout(this._updateThumbnailTimeout);
Elements.tabList.removeTab(this._chromeTab);
diff --git a/browser/metro/base/content/browser.xul b/browser/metro/base/content/browser.xul
index fc5c3d96a04..32eeea5d1f3 100644
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -260,7 +260,7 @@ Desktop browser's sync prefs.
-
.toolbarbutton-text {
/* Sprites */
-.appbar-primary > .toolbarbutton-icon,
-.appbar-secondary > .toolbarbutton-icon {
+.appbar-primary .toolbarbutton-icon,
+.appbar-secondary .toolbarbutton-icon {
width: 40px;
height: 40px;
}
diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css
index 1bd8229b47b..dd4caa66e2d 100644
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -700,6 +700,10 @@ menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip) {
color: GrayText;
}
+#search-container {
+ min-width: calc(54px + 11ch);
+}
+
%include ../shared/identity-block.inc.css
#page-proxy-favicon {
diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css
index f8d9003962d..984e60115a9 100644
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1674,6 +1674,10 @@ toolbar .toolbarbutton-1:not([type="menu-button"]),
height: 22px;
}
+#search-container {
+ min-width: calc(54px + 11ch);
+}
+
%include ../shared/identity-block.inc.css
#page-proxy-favicon {
diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css
index ad9382b9589..56f59c27621 100644
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -956,6 +956,10 @@ html|*.urlbar-input:-moz-lwtheme::-moz-placeholder,
color: GrayText;
}
+#search-container {
+ min-width: calc(54px + 11ch);
+}
+
/* identity box */
#identity-box {
diff --git a/build/autoconf/android.m4 b/build/autoconf/android.m4
index 1231d3718c2..2745117bdae 100644
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -259,6 +259,8 @@ MOZ_ARG_WITH_STRING(android-sdk,
location where the Android SDK can be found (base directory, e.g. .../android/platforms/android-6)],
android_sdk=$withval)
+android_sdk_root=$withval/../../
+
case "$target" in
*-android*|*-linuxandroid*)
if test -z "$android_sdk" ; then
@@ -284,8 +286,8 @@ case "$target" in
fi
fi
- android_tools="$android_sdk"/../../tools
- android_platform_tools="$android_sdk"/../../platform-tools
+ android_tools="$android_sdk_root"/tools
+ android_platform_tools="$android_sdk_root"/platform-tools
if test ! -d "$android_platform_tools" ; then
android_platform_tools="$android_sdk"/tools # SDK Tools < r8
fi
@@ -293,7 +295,7 @@ case "$target" in
# SDK Tools r22. Try to locate them.
android_build_tools=""
for suffix in android-4.3 19.0.0 18.1.0 18.0.1 18.0.0 17.0.0 android-4.2.2; do
- tools_directory="$android_sdk/../../build-tools/$suffix"
+ tools_directory="$android_sdk_root/build-tools/$suffix"
if test -d "$tools_directory" ; then
android_build_tools="$tools_directory"
break
@@ -303,14 +305,16 @@ case "$target" in
android_build_tools="$android_platform_tools" # SDK Tools < r22
fi
ANDROID_SDK="${android_sdk}"
- if test -e "${android_sdk}/../../extras/android/compatibility/v4/android-support-v4.jar" ; then
- ANDROID_COMPAT_LIB="${android_sdk}/../../extras/android/compatibility/v4/android-support-v4.jar"
+ ANDROID_SDK_ROOT="${android_sdk_root}"
+ if test -e "${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar" ; then
+ ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar"
else
- ANDROID_COMPAT_LIB="${android_sdk}/../../extras/android/support/v4/android-support-v4.jar";
+ ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/support/v4/android-support-v4.jar";
fi
ANDROID_TOOLS="${android_tools}"
ANDROID_PLATFORM_TOOLS="${android_platform_tools}"
ANDROID_BUILD_TOOLS="${android_build_tools}"
+ AC_SUBST(ANDROID_SDK_ROOT)
AC_SUBST(ANDROID_SDK)
AC_SUBST(ANDROID_COMPAT_LIB)
if ! test -e $ANDROID_COMPAT_LIB ; then
diff --git a/js/src/build/autoconf/android.m4 b/js/src/build/autoconf/android.m4
index 1231d3718c2..2745117bdae 100644
--- a/js/src/build/autoconf/android.m4
+++ b/js/src/build/autoconf/android.m4
@@ -259,6 +259,8 @@ MOZ_ARG_WITH_STRING(android-sdk,
location where the Android SDK can be found (base directory, e.g. .../android/platforms/android-6)],
android_sdk=$withval)
+android_sdk_root=$withval/../../
+
case "$target" in
*-android*|*-linuxandroid*)
if test -z "$android_sdk" ; then
@@ -284,8 +286,8 @@ case "$target" in
fi
fi
- android_tools="$android_sdk"/../../tools
- android_platform_tools="$android_sdk"/../../platform-tools
+ android_tools="$android_sdk_root"/tools
+ android_platform_tools="$android_sdk_root"/platform-tools
if test ! -d "$android_platform_tools" ; then
android_platform_tools="$android_sdk"/tools # SDK Tools < r8
fi
@@ -293,7 +295,7 @@ case "$target" in
# SDK Tools r22. Try to locate them.
android_build_tools=""
for suffix in android-4.3 19.0.0 18.1.0 18.0.1 18.0.0 17.0.0 android-4.2.2; do
- tools_directory="$android_sdk/../../build-tools/$suffix"
+ tools_directory="$android_sdk_root/build-tools/$suffix"
if test -d "$tools_directory" ; then
android_build_tools="$tools_directory"
break
@@ -303,14 +305,16 @@ case "$target" in
android_build_tools="$android_platform_tools" # SDK Tools < r22
fi
ANDROID_SDK="${android_sdk}"
- if test -e "${android_sdk}/../../extras/android/compatibility/v4/android-support-v4.jar" ; then
- ANDROID_COMPAT_LIB="${android_sdk}/../../extras/android/compatibility/v4/android-support-v4.jar"
+ ANDROID_SDK_ROOT="${android_sdk_root}"
+ if test -e "${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar" ; then
+ ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar"
else
- ANDROID_COMPAT_LIB="${android_sdk}/../../extras/android/support/v4/android-support-v4.jar";
+ ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/support/v4/android-support-v4.jar";
fi
ANDROID_TOOLS="${android_tools}"
ANDROID_PLATFORM_TOOLS="${android_platform_tools}"
ANDROID_BUILD_TOOLS="${android_build_tools}"
+ AC_SUBST(ANDROID_SDK_ROOT)
AC_SUBST(ANDROID_SDK)
AC_SUBST(ANDROID_COMPAT_LIB)
if ! test -e $ANDROID_COMPAT_LIB ; then
diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js
index 310753d8bb4..8a47d70057f 100644
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -800,5 +800,8 @@ pref("browser.snippets.updateInterval", 86400);
// URL used to check for user's country code
pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
+// URL used to ping metrics with stats about which snippets have been shown
+pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
+
// This pref requires a restart to take effect.
pref("browser.snippets.enabled", false);
diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in
index cbc56cd4806..6d3c90d9408 100644
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -86,9 +86,19 @@ include $(topsrcdir)/config/config.mk
# Sync dependencies are provided in a single jar. Sync classes themselves are delivered as source,
# because Android resource classes must be compiled together in order to avoid overlapping resource
# indices.
-classes.dex: $(ALL_JARS)
+
+classes.dex: proguard-jars
@echo 'DX classes.dex'
- $(DX) --dex --output=classes.dex $(ALL_JARS) $(ANDROID_COMPAT_LIB)
+ $(DX) --dex --output=classes.dex jars-proguarded $(ANDROID_COMPAT_LIB)
+
+ifdef MOZ_DEBUG
+PROGUARD_PASSES=1
+else
+PROGUARD_PASSES=6
+endif
+
+proguard-jars: $(ALL_JARS)
+ java -jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar @$(topsrcdir)/mobile/android/config/proguard.cfg -optimizationpasses $(PROGUARD_PASSES) -injars $(subst ::,:,$(subst $(NULL) ,:,$(ALL_JARS))) -outjars jars-proguarded -libraryjars $(ANDROID_SDK)/android.jar:$(ANDROID_COMPAT_LIB)
CLASSES_WITH_JNI= \
org.mozilla.gecko.GeckoAppShell \
diff --git a/mobile/android/components/Snippets.js b/mobile/android/components/Snippets.js
index b9337e56974..c9b8c55546b 100644
--- a/mobile/android/components/Snippets.js
+++ b/mobile/android/components/Snippets.js
@@ -9,6 +9,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Home", "resource://gre/modules/Home.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { return new gChromeWin.TextEncoder(); });
XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { return new gChromeWin.TextDecoder(); });
@@ -18,6 +19,9 @@ const SNIPPETS_ENABLED = Services.prefs.getBoolPref("browser.snippets.enabled");
// URL to fetch snippets, in the urlFormatter service format.
const SNIPPETS_UPDATE_URL_PREF = "browser.snippets.updateUrl";
+// URL to send stats data to metrics.
+const SNIPPETS_STATS_URL_PREF = "browser.snippets.statsUrl";
+
// URL to fetch country code, a value that's cached and refreshed once per month.
const SNIPPETS_GEO_URL_PREF = "browser.snippets.geoUrl";
@@ -38,6 +42,20 @@ XPCOMUtils.defineLazyGetter(this, "gSnippetsURL", function() {
return Services.urlFormatter.formatURL(updateURL);
});
+// Where we cache snippets data
+XPCOMUtils.defineLazyGetter(this, "gSnippetsPath", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gStatsURL", function() {
+ return Services.prefs.getCharPref(SNIPPETS_STATS_URL_PREF);
+});
+
+// Where we store stats about which snippets have been shown
+XPCOMUtils.defineLazyGetter(this, "gStatsPath", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "snippets-stats.txt");
+});
+
XPCOMUtils.defineLazyGetter(this, "gGeoURL", function() {
return Services.prefs.getCharPref(SNIPPETS_GEO_URL_PREF);
});
@@ -109,9 +127,8 @@ function updateSnippets() {
* @param response responseText returned from snippets server
*/
function cacheSnippets(response) {
- let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
let data = gEncoder.encode(response);
- let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" });
+ let promise = OS.File.writeAtomic(gSnippetsPath, data, { tmpPath: gSnippetsPath + ".tmp" });
promise.then(null, e => Cu.reportError("Error caching snippets: " + e));
}
@@ -119,8 +136,7 @@ function cacheSnippets(response) {
* Loads snippets from cached `snippets.json`.
*/
function loadSnippetsFromCache() {
- let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
- let promise = OS.File.read(path);
+ let promise = OS.File.read(gSnippetsPath);
promise.then(array => updateBanner(gDecoder.decode(array)), e => {
// If snippets.json doesn't exist, update data from the server.
if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
@@ -167,7 +183,10 @@ function updateBanner(response) {
gChromeWin.BrowserApp.addTab(message.url);
},
onshown: function() {
- // XXX: 10% of the time, let the metrics server know which message was shown (bug 937373)
+ // 10% of the time, record the snippet id and a timestamp
+ if (Math.random() < .1) {
+ writeStat(message.id, new Date().toISOString());
+ }
}
});
// Keep track of the message we added so that we can remove it later.
@@ -175,6 +194,76 @@ function updateBanner(response) {
});
}
+/**
+ * Appends snippet id and timestamp to the end of `snippets-stats.txt`.
+ *
+ * @param snippetId unique id for snippet, sent from snippets server
+ * @param timestamp in ISO8601
+ */
+function writeStat(snippetId, timestamp) {
+ let data = gEncoder.encode(snippetId + "," + timestamp + ";");
+
+ Task.spawn(function() {
+ try {
+ let file = yield OS.File.open(gStatsPath, { append: true, write: true });
+ try {
+ yield file.write(data);
+ } finally {
+ yield file.close();
+ }
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ // If the file doesn't exist yet, create it.
+ yield OS.File.writeAtomic(gStatsPath, data, { tmpPath: gStatsPath + ".tmp" });
+ }
+ }).then(null, e => Cu.reportError("Error writing snippets stats: " + e));
+}
+
+/**
+ * Reads snippets stats data from `snippets-stats.txt` and sends the data to metrics.
+ */
+function sendStats() {
+ let promise = OS.File.read(gStatsPath);
+ promise.then(array => sendStatsRequest(gDecoder.decode(array)), e => {
+ if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ // If the file doesn't exist, there aren't any stats to send.
+ } else {
+ Cu.reportError("Error eading snippets stats: " + e);
+ }
+ });
+}
+
+/**
+ * Sends stats to metrics about which snippets have been shown.
+ * Appends snippet ids and timestamps as parameters to a GET request.
+ * e.g. https://snippets-stats.mozilla.org/mobile?s1=3825&t1=2013-11-17T18:27Z&s2=6326&t2=2013-11-18T18:27Z
+ *
+ * @param data contents of stats data file
+ */
+function sendStatsRequest(data) {
+ let params = [];
+ let stats = data.split(";");
+
+ // The last item in the array will be an empty string, so stop before then.
+ for (let i = 0; i < stats.length - 1; i++) {
+ let stat = stats[i].split(",");
+ params.push("s" + i + "=" + encodeURIComponent(stat[0]));
+ params.push("t" + i + "=" + encodeURIComponent(stat[1]));
+ }
+
+ let url = gStatsURL + "?" + params.join("&");
+
+ // Remove the file after succesfully sending the data.
+ _httpGetRequest(url, removeStats);
+}
+
+/**
+ * Removes text file where we store snippets stats.
+ */
+function removeStats() {
+ let promise = OS.File.remove(gStatsPath);
+ promise.then(null, e => Cu.reportError("Error removing snippets stats: " + e));
+}
+
/**
* Helper function to make HTTP GET requests.
*
@@ -227,6 +316,7 @@ Snippets.prototype = {
return;
}
update();
+ sendStats();
}
};
diff --git a/mobile/android/config/proguard.cfg b/mobile/android/config/proguard.cfg
new file mode 100644
index 00000000000..feae3552e87
--- /dev/null
+++ b/mobile/android/config/proguard.cfg
@@ -0,0 +1,201 @@
+# Dalvik renders preverification unuseful (Would just slightly bloat the file).
+-dontpreverify
+
+# Uncomment to have Proguard list dead code detected during the run - useful for cleaning up the codebase.
+# -printusage
+
+-dontskipnonpubliclibraryclassmembers
+-verbose
+-allowaccessmodification
+
+# Preserve all fundamental application classes.
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.preference.Preference
+-keep public class * extends org.mozilla.gecko.sync.syncadapter.SyncAdapter
+-keep class org.mozilla.gecko.sync.syncadapter.SyncAdapter
+
+# Preserve all native method names and the names of their classes.
+-keepclasseswithmembernames class * {
+ native ;
+}
+
+-keepclasseswithmembers class * {
+ public (android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers class * extends android.app.Activity {
+ public void *(android.view.View);
+}
+
+# Preserve enums. (For awful reasons, the runtime accesses them using introspection...)
+-keepclassmembers enum * {
+ *;
+}
+
+#
+# Rules from ProGuard's Android example:
+# http://proguard.sourceforge.net/manual/examples.html#androidapplication
+#
+
+# Switch off some optimizations that trip older versions of the Dalvik VM.
+
+-optimizations !code/simplification/arithmetic
+
+# Keep a fixed source file attribute and all line number tables to get line
+# numbers in the stack traces.
+# You can comment this out if you're not interested in stack traces.
+
+-renamesourcefileattribute SourceFile
+-keepattributes SourceFile,LineNumberTable
+
+# RemoteViews might need annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all View implementations, their special context constructors, and
+# their setters.
+
+-keep public class * extends android.view.View {
+ public (android.content.Context);
+ public (android.content.Context, android.util.AttributeSet);
+ public (android.content.Context, android.util.AttributeSet, int);
+ public void set*(...);
+}
+
+# Preserve all classes that have special context constructors, and the
+# constructors themselves.
+
+-keepclasseswithmembers class * {
+ public (android.content.Context, android.util.AttributeSet);
+}
+
+# Preserve the special fields of all Parcelable implementations.
+
+-keepclassmembers class * implements android.os.Parcelable {
+ static android.os.Parcelable$Creator CREATOR;
+}
+
+# Preserve static fields of inner classes of R classes that might be accessed
+# through introspection.
+
+-keepclassmembers class **.R$* {
+ public static ;
+}
+
+# Preserve the required interface from the License Verification Library
+# (but don't nag the developer if the library is not used at all).
+
+-keep public interface com.android.vending.licensing.ILicensingService
+
+-dontnote com.android.vending.licensing.ILicensingService
+
+# The Android Compatibility library references some classes that may not be
+# present in all versions of the API, but we know that's ok.
+
+-dontwarn android.support.**
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+ native ;
+}
+
+#
+# Mozilla-specific rules
+#
+# Merging classes can generate dex warnings about anonymous inner classes.
+-optimizations !class/merging/horizontal
+-optimizations !class/merging/vertical
+
+# Keep miscellaneous targets.
+
+# Keep the annotation.
+-keep @interface org.mozilla.gecko.mozglue.JNITarget
+
+# Keep classes tagged with the annotation.
+-keep @org.mozilla.gecko.mozglue.JNITarget class *
+
+# Keep all members of an annotated class.
+-keepclassmembers @org.mozilla.gecko.mozglue.JNITarget class * {
+ *;
+}
+
+# Keep annotated members of any class.
+-keepclassmembers class * {
+ @org.mozilla.gecko.mozglue.JNITarget *;
+}
+
+# Keep classes which contain at least one annotated element. Split over two directives
+# because, according to the developer of ProGuard, "the option -keepclasseswithmembers
+# doesn't combine well with the '*' wildcard" (And, indeed, using it causes things to
+# be deleted that we want to keep.)
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.mozglue.JNITarget ;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.mozglue.JNITarget ;
+}
+
+# Keep Robocop targets. TODO: Can omit these from release builds. Also, Bug 916507.
+
+# Same formula as above...
+-keep @interface org.mozilla.gecko.mozglue.RobocopTarget
+-keep @org.mozilla.gecko.mozglue.RobocopTarget class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.mozglue.RobocopTarget *;
+}
+-keepclassmembers @org.mozilla.gecko.mozglue.RobocopTarget class * {
+ *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.mozglue.RobocopTarget ;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.mozglue.RobocopTarget ;
+}
+
+# Keep WebRTC targets.
+-keep @interface org.mozilla.gecko.mozglue.WebRTCJNITarget
+-keep @org.mozilla.gecko.mozglue.WebRTCJNITarget class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.mozglue.WebRTCJNITarget *;
+}
+-keepclassmembers @org.mozilla.gecko.mozglue.WebRTCJNITarget class * {
+ *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.mozglue.WebRTCJNITarget ;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.mozglue.WebRTCJNITarget ;
+}
+
+# Keep generator-targeted entry points.
+-keep @interface org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI
+-keep @org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI class *
+-keepclassmembers class * {
+ @org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI *;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI ;
+}
+-keepclasseswithmembers class * {
+ @org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI ;
+}
+
+-keep @interface org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI
+-keep @org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI class *
+-keepclassmembers @org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI class * {
+ *;
+}
+
+# Disable obfuscation because it makes exception stack traces more difficult to read.
+-dontobfuscate
+
+# Suppress warnings about missing descriptor classes.
+#-dontnote **,!ch.boye.**,!org.mozilla.gecko.sync.**
diff --git a/mozglue/build/BionicGlue.cpp b/mozglue/build/BionicGlue.cpp
index 7904c2a9774..d9591138aed 100644
--- a/mozglue/build/BionicGlue.cpp
+++ b/mozglue/build/BionicGlue.cpp
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include "mozilla/Alignment.h"
@@ -128,7 +129,16 @@ WRAP(fork)(void)
extern "C" NS_EXPORT int
WRAP(raise)(int sig)
{
- return pthread_kill(pthread_self(), sig);
+ // Bug 741272: Bionic incorrectly uses kill(), which signals the
+ // process, and thus could signal another thread (and let this one
+ // return "successfully" from raising a fatal signal).
+ //
+ // Bug 943170: POSIX specifies pthread_kill(pthread_self(), sig) as
+ // equivalent to raise(sig), but Bionic also has a bug with these
+ // functions, where a forked child will kill its parent instead.
+
+ extern pid_t gettid(void);
+ return syscall(__NR_tgkill, getpid(), gettid(), sig);
}
/*
diff --git a/services/common/tests/unit/head_helpers.js b/services/common/tests/unit/head_helpers.js
index c02f4535cf6..039b20219ad 100644
--- a/services/common/tests/unit/head_helpers.js
+++ b/services/common/tests/unit/head_helpers.js
@@ -35,6 +35,26 @@ function do_check_throws(aFunc, aResult, aStack) {
do_throw("Expected result " + aResult + ", none thrown.", aStack);
}
+
+/**
+ * Test whether specified function throws exception with expected
+ * result.
+ *
+ * @param func
+ * Function to be tested.
+ * @param message
+ * Message of expected exception. null
for no throws.
+ */
+function do_check_throws_message(aFunc, aResult) {
+ try {
+ aFunc();
+ } catch (e) {
+ do_check_eq(e.message, aResult);
+ return;
+ }
+ do_throw("Expected an error, none thrown.");
+}
+
/**
* Print some debug message to the console. All arguments will be printed,
* separated by spaces.
diff --git a/services/common/utils.js b/services/common/utils.js
index ca5858ccb84..d99781a719b 100644
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -204,6 +204,14 @@ this.CommonUtils = {
return hex;
},
+ hexToBytes: function hexToBytes(str) {
+ let bytes = [];
+ for (let i = 0; i < str.length - 1; i += 2) {
+ bytes.push(parseInt(str.substr(i, 2), 16));
+ }
+ return String.fromCharCode.apply(String, bytes);
+ },
+
/**
* Base32 encode (RFC 4648) a string
*/
diff --git a/services/crypto/modules/utils.js b/services/crypto/modules/utils.js
index ac59f6a351b..605b8d3a428 100644
--- a/services/crypto/modules/utils.js
+++ b/services/crypto/modules/utils.js
@@ -11,6 +11,20 @@ Cu.import("resource://services-common/utils.js");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.CryptoUtils = {
+ xor: function xor(a, b) {
+ let bytes = [];
+
+ if (a.length != b.length) {
+ throw new Error("can't xor unequal length strings: "+a.length+" vs "+b.length);
+ }
+
+ for (let i = 0; i < a.length; i++) {
+ bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i);
+ }
+
+ return String.fromCharCode.apply(String, bytes);
+ },
+
/**
* Generate a string of random bytes.
*/
@@ -109,6 +123,22 @@ this.CryptoUtils = {
return hasher;
},
+ /**
+ * HMAC-based Key Derivation (RFC 5869).
+ */
+ hkdf: function hkdf(ikm, xts, info, len) {
+ const BLOCKSIZE = 256 / 8;
+ if (typeof xts === undefined)
+ xts = String.fromCharCode(0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0);
+ let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
+ CryptoUtils.makeHMACKey(xts));
+ let prk = CryptoUtils.digestBytes(ikm, h);
+ return CryptoUtils.hkdfExpand(prk, info, len);
+ },
+
/**
* HMAC-based Key Derivation Step 2 according to RFC 5869.
*/
@@ -458,9 +488,8 @@ this.CryptoUtils = {
let contentType = CryptoUtils.stripHeaderAttributes(options.contentType);
- if (!artifacts.hash &&
- options.hasOwnProperty("payload") &&
- options.payload) {
+ if (!artifacts.hash && options.hasOwnProperty("payload")
+ && options.payload) {
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hash_algo);
@@ -469,8 +498,9 @@ this.CryptoUtils = {
CryptoUtils.updateUTF8(options.payload, hasher);
CryptoUtils.updateUTF8("\n", hasher);
let hash = hasher.finish(false);
- // HAWK specifies this .hash to include trailing "==" padding.
- let hash_b64 = CommonUtils.encodeBase64URL(hash, true);
+ // HAWK specifies this .hash to use +/ (not _-) and include the
+ // trailing "==" padding.
+ let hash_b64 = btoa(hash);
artifacts.hash = hash_b64;
}
diff --git a/services/crypto/tests/unit/test_utils_hkdfExpand.js b/services/crypto/tests/unit/test_utils_hkdfExpand.js
index 2a849b035c0..47ddd7796a8 100644
--- a/services/crypto/tests/unit/test_utils_hkdfExpand.js
+++ b/services/crypto/tests/unit/test_utils_hkdfExpand.js
@@ -93,16 +93,28 @@ function expand_hex(prk, info, len) {
return CommonUtils.bytesAsHex(CryptoUtils.hkdfExpand(prk, info, len));
}
+function hkdf_hex(ikm, salt, info, len) {
+ ikm = _hexToString(ikm);
+ if (salt)
+ salt = _hexToString(salt);
+ info = _hexToString(info);
+ return CommonUtils.bytesAsHex(CryptoUtils.hkdf(ikm, salt, info, len));
+}
+
function run_test() {
_("Verifying Test Case 1");
do_check_eq(extract_hex(tc1.salt, tc1.IKM), tc1.PRK);
do_check_eq(expand_hex(tc1.PRK, tc1.info, tc1.L), tc1.OKM);
+ do_check_eq(hkdf_hex(tc1.IKM, tc1.salt, tc1.info, tc1.L), tc1.OKM);
_("Verifying Test Case 2");
do_check_eq(extract_hex(tc2.salt, tc2.IKM), tc2.PRK);
do_check_eq(expand_hex(tc2.PRK, tc2.info, tc2.L), tc2.OKM);
+ do_check_eq(hkdf_hex(tc2.IKM, tc2.salt, tc2.info, tc2.L), tc2.OKM);
_("Verifying Test Case 3");
do_check_eq(extract_hex(tc3.salt, tc3.IKM), tc3.PRK);
do_check_eq(expand_hex(tc3.PRK, tc3.info, tc3.L), tc3.OKM);
+ do_check_eq(hkdf_hex(tc3.IKM, tc3.salt, tc3.info, tc3.L), tc3.OKM);
+ do_check_eq(hkdf_hex(tc3.IKM, undefined, tc3.info, tc3.L), tc3.OKM);
}
diff --git a/services/fxaccounts/FxAccountsClient.jsm b/services/fxaccounts/FxAccountsClient.jsm
new file mode 100644
index 00000000000..5f5df829b77
--- /dev/null
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -0,0 +1,330 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
+
+// Default can be changed by the preference 'identity.fxaccounts.auth.uri'
+let _host = "https://api-accounts.dev.lcip.org/v1";
+try {
+ _host = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
+} catch(keepDefault) {}
+
+const HOST = _host;
+const PREFIX_NAME = "identity.mozilla.com/picl/v1/";
+
+const XMLHttpRequest =
+ Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
+
+
+function stringToHex(str) {
+ let encoder = new TextEncoder("utf-8");
+ let bytes = encoder.encode(str);
+ return bytesToHex(bytes);
+}
+
+// XXX Sadly, CommonUtils.bytesAsHex doesn't handle typed arrays.
+function bytesToHex(bytes) {
+ let hex = [];
+ for (let i = 0; i < bytes.length; i++) {
+ hex.push((bytes[i] >>> 4).toString(16));
+ hex.push((bytes[i] & 0xF).toString(16));
+ }
+ return hex.join("");
+}
+
+this.FxAccountsClient = function(host = HOST) {
+ this.host = host;
+};
+
+this.FxAccountsClient.prototype = {
+ /**
+ * Create a new Firefox Account and authenticate
+ *
+ * @param email
+ * The email address for the account (utf8)
+ * @param password
+ * The user's password
+ * @return Promise
+ * Returns a promise that resolves to an object:
+ * {
+ * uid: the user's unique ID
+ * sessionToken: a session token
+ * }
+ */
+ signUp: function (email, password) {
+ let uid;
+ let hexEmail = stringToHex(email);
+ let uidPromise = this._request("/raw_password/account/create", "POST", null,
+ {email: hexEmail, password: password});
+
+ return uidPromise.then((result) => {
+ uid = result.uid;
+ return this.signIn(email, password)
+ .then(function(result) {
+ result.uid = uid;
+ return result;
+ });
+ });
+ },
+
+ /**
+ * Authenticate and create a new session with the Firefox Account API server
+ *
+ * @param email
+ * The email address for the account (utf8)
+ * @param password
+ * The user's password
+ * @return Promise
+ * Returns a promise that resolves to an object:
+ * {
+ * uid: the user's unique ID
+ * sessionToken: a session token
+ * isVerified: flag indicating verification status of the email
+ * }
+ */
+ signIn: function signIn(email, password) {
+ let hexEmail = stringToHex(email);
+ return this._request("/raw_password/session/create", "POST", null,
+ {email: hexEmail, password: password});
+ },
+
+ /**
+ * Destroy the current session with the Firefox Account API server
+ *
+ * @param sessionTokenHex
+ * The session token endcoded in hex
+ * @return Promise
+ */
+ signOut: function (sessionTokenHex) {
+ return this._request("/session/destroy", "POST",
+ this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
+ },
+
+ /**
+ * Check the verification status of the user's FxA email address
+ *
+ * @param sessionTokenHex
+ * The current session token endcoded in hex
+ * @return Promise
+ */
+ recoveryEmailStatus: function (sessionTokenHex) {
+ return this._request("/recovery_email/status", "GET",
+ this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
+ },
+
+ /**
+ * Retrieve encryption keys
+ *
+ * @param keyFetchTokenHex
+ * A one-time use key fetch token encoded in hex
+ * @return Promise
+ * Returns a promise that resolves to an object:
+ * {
+ * kA: an encryption key for recevorable data
+ * wrapKB: an encryption key that requires knowledge of the user's password
+ * }
+ */
+ accountKeys: function (keyFetchTokenHex) {
+ let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
+ let keyRequestKey = creds.extra.slice(0, 32);
+ let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
+ PREFIX_NAME + "account/keys", 3 * 32);
+ let respHMACKey = morecreds.slice(0, 32);
+ let respXORKey = morecreds.slice(32, 96);
+
+ return this._request("/account/keys", "GET", creds).then(resp => {
+ if (!resp.bundle) {
+ throw new Error("failed to retrieve keys");
+ }
+
+ let bundle = CommonUtils.hexToBytes(resp.bundle);
+ let mac = bundle.slice(-32);
+
+ let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
+ CryptoUtils.makeHMACKey(respHMACKey));
+
+ let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher);
+ if (mac !== bundleMAC) {
+ throw new Error("error unbundling encryption keys");
+ }
+
+ let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
+
+ return {
+ kA: keyAWrapB.slice(0, 32),
+ wrapKB: keyAWrapB.slice(32)
+ };
+ });
+ },
+
+ /**
+ * Sends a public key to the FxA API server and returns a signed certificate
+ *
+ * @param sessionTokenHex
+ * The current session token endcoded in hex
+ * @param serializedPublicKey
+ * A public key (usually generated by jwcrypto)
+ * @param lifetime
+ * The lifetime of the certificate
+ * @return Promise
+ * Returns a promise that resolves to the signed certificate. The certificate
+ * can be used to generate a Persona assertion.
+ */
+ signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) {
+ let creds = this._deriveHawkCredentials(sessionTokenHex, "sessionToken");
+
+ let body = { publicKey: serializedPublicKey,
+ duration: lifetime };
+ return Promise.resolve()
+ .then(_ => this._request("/certificate/sign", "POST", creds, body))
+ .then(resp => resp.cert,
+ err => {dump("HAWK.signCertificate error: " + err + "\n");
+ throw err;});
+ },
+
+ /**
+ * Determine if an account exists
+ *
+ * @param email
+ * The email address to check
+ * @return Promise
+ * The promise resolves to true if the account exists, or false
+ * if it doesn't. The promise is rejected on other errors.
+ */
+ accountExists: function (email) {
+ let hexEmail = stringToHex(email);
+ return this._request("/auth/start", "POST", null, { email: hexEmail })
+ .then(
+ // the account exists
+ (result) => true,
+ (err) => {
+ // the account doesn't exist
+ if (err.errno === 102) {
+ return false;
+ }
+ // propogate other request errors
+ throw err;
+ }
+ );
+ },
+
+ /**
+ * The FxA auth server expects requests to certain endpoints to be authorized using Hawk.
+ * Hawk credentials are derived using shared secrets, which depend on the context
+ * (e.g. sessionToken vs. keyFetchToken).
+ *
+ * @param tokenHex
+ * The current session token endcoded in hex
+ * @param context
+ * A context for the credentials
+ * @param size
+ * The size in bytes of the expected derived buffer
+ * @return credentials
+ * Returns an object:
+ * {
+ * algorithm: sha256
+ * id: the Hawk id (from the first 32 bytes derived)
+ * key: the Hawk key (from bytes 32 to 64)
+ * extra: size - 64 extra bytes
+ * }
+ */
+ _deriveHawkCredentials: function (tokenHex, context, size) {
+ let token = CommonUtils.hexToBytes(tokenHex);
+ let out = CryptoUtils.hkdf(token, undefined, PREFIX_NAME + context, size || 3 * 32);
+
+ return {
+ algorithm: "sha256",
+ key: out.slice(32, 64),
+ extra: out.slice(64),
+ id: CommonUtils.bytesAsHex(out.slice(0, 32))
+ };
+ },
+
+ /**
+ * A general method for sending raw API calls to the FxA auth server.
+ * All request bodies and responses are JSON.
+ *
+ * @param path
+ * API endpoint path
+ * @param method
+ * The HTTP request method
+ * @param credentials
+ * Hawk credentials
+ * @param jsonPayload
+ * A JSON payload
+ * @return Promise
+ * Returns a promise that resolves to the JSON response of the API call,
+ * or is rejected with an error. Error responses have the following properties:
+ * {
+ * "code": 400, // matches the HTTP status code
+ * "errno": 107, // stable application-level error number
+ * "error": "Bad Request", // string description of the error type
+ * "message": "the value of salt is not allowed to be undefined",
+ * "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
+ * }
+ */
+ _request: function hawkRequest(path, method, credentials, jsonPayload) {
+ let deferred = Promise.defer();
+ let xhr = new XMLHttpRequest({mozSystem: true});
+ let URI = this.host + path;
+ let payload;
+
+ xhr.mozBackgroundRequest = true;
+
+ if (jsonPayload) {
+ payload = JSON.stringify(jsonPayload);
+ }
+
+ xhr.open(method, URI);
+ xhr.channel.loadFlags = Ci.nsIChannel.LOAD_BYPASS_CACHE |
+ Ci.nsIChannel.INHIBIT_CACHING;
+
+ // When things really blow up, reconstruct an error object that follows the general format
+ // of the server on error responses.
+ function constructError(err) {
+ return { error: err, message: xhr.statusText, code: xhr.status, errno: xhr.status };
+ }
+
+ xhr.onerror = function() {
+ deferred.reject(constructError('Request failed'));
+ };
+
+ xhr.onload = function onload() {
+ try {
+ let response = JSON.parse(xhr.responseText);
+ if (xhr.status !== 200 || response.error) {
+ // In this case, the response is an object with error information.
+ return deferred.reject(response);
+ }
+ deferred.resolve(response);
+ } catch (e) {
+ deferred.reject(constructError(e));
+ }
+ };
+
+ let uri = Services.io.newURI(URI, null, null);
+
+ if (credentials) {
+ let header = CryptoUtils.computeHAWK(uri, method, {
+ credentials: credentials,
+ payload: payload,
+ contentType: "application/json"
+ });
+ xhr.setRequestHeader("authorization", header.field);
+ }
+
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.send(payload);
+
+ return deferred.promise;
+ },
+};
+
diff --git a/services/fxaccounts/moz.build b/services/fxaccounts/moz.build
new file mode 100644
index 00000000000..d3b43cdcd5a
--- /dev/null
+++ b/services/fxaccounts/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += ['tests']
+EXTRA_JS_MODULES += ['FxAccountsClient.jsm']
diff --git a/services/fxaccounts/tests/moz.build b/services/fxaccounts/tests/moz.build
new file mode 100644
index 00000000000..dcad37e80e1
--- /dev/null
+++ b/services/fxaccounts/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['xpcshell/xpcshell.ini']
diff --git a/services/fxaccounts/tests/xpcshell/head.js b/services/fxaccounts/tests/xpcshell/head.js
new file mode 100644
index 00000000000..0a0bfc57e20
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/head.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+"use strict";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+(function initFxAccountsTestingInfrastructure() {
+ do_get_profile();
+
+ let ns = {};
+ Cu.import("resource://testing-common/services-common/logging.js", ns);
+
+ ns.initTestLogging("Trace");
+}).call(this);
+
diff --git a/services/fxaccounts/tests/xpcshell/test_client.js b/services/fxaccounts/tests/xpcshell/test_client.js
new file mode 100644
index 00000000000..ff38c39795a
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_client.js
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://services-common/utils.js");
+
+function run_test() {
+ run_next_test();
+}
+
+function deferredStop(server) {
+ let deferred = Promise.defer();
+ server.stop(deferred.resolve);
+ return deferred.promise;
+}
+
+add_test(function test_hawk_credentials() {
+ let client = new FxAccountsClient();
+
+ let sessionToken = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
+ let result = client._deriveHawkCredentials(sessionToken, "session");
+
+ do_check_eq(result.id, "639503a218ffbb62983e9628be5cd64a0438d0ae81b2b9dadeb900a83470bc6b");
+ do_check_eq(CommonUtils.bytesAsHex(result.key), "3a0188943837ab228fe74e759566d0e4837cbcc7494157aac4da82025b2811b2");
+
+ run_next_test();
+});
+
+add_task(function test_authenticated_get_request() {
+
+ let message = "{\"msg\": \"Great Success!\"}";
+ let credentials = {
+ id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+ key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+ algorithm: "sha256"
+ };
+ let method = "GET";
+
+ let server = httpd_setup({"/foo": function(request, response) {
+ do_check_true(request.hasHeader("Authorization"));
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(message, message.length);
+ }
+ });
+
+ let client = new FxAccountsClient(server.baseURI);
+
+ let result = yield client._request("/foo", method, credentials);
+ do_check_eq("Great Success!", result.msg);
+
+ yield deferredStop(server);
+});
+
+add_task(function test_authenticated_post_request() {
+ let credentials = {
+ id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+ key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+ algorithm: "sha256"
+ };
+ let method = "POST";
+
+ let server = httpd_setup({"/foo": function(request, response) {
+ do_check_true(request.hasHeader("Authorization"));
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json");
+ response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available());
+ }
+ });
+
+ let client = new FxAccountsClient(server.baseURI);
+
+ let result = yield client._request("/foo", method, credentials, {foo: "bar"});
+ do_check_eq("bar", result.foo);
+
+ yield deferredStop(server);
+});
+
+add_task(function test_500_error() {
+
+ let message = "Ooops!
";
+ let method = "GET";
+
+ let server = httpd_setup({"/foo": function(request, response) {
+ response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
+ response.bodyOutputStream.write(message, message.length);
+ }
+ });
+
+ let client = new FxAccountsClient(server.baseURI);
+
+ try {
+ yield client._request("/foo", method);
+ } catch (e) {
+ do_check_eq(500, e.code);
+ do_check_eq("Internal Server Error", e.message);
+ }
+
+ yield deferredStop(server);
+});
+
+add_task(function test_api_endpoints() {
+ let sessionMessage = JSON.stringify({sessionToken: "NotARealToken"});
+ let creationMessage = JSON.stringify({uid: "NotARealUid"});
+ let signoutMessage = JSON.stringify({});
+ let certSignMessage = JSON.stringify({cert: {bar: "baz"}});
+ let emailStatus = JSON.stringify({verified: true});
+
+ let authStarts = 0;
+
+ function writeResp(response, msg) {
+ response.bodyOutputStream.write(msg, msg.length);
+ }
+
+ let server = httpd_setup(
+ {
+ "/raw_password/account/create": function(request, response) {
+ let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ let jsonBody = JSON.parse(body);
+ do_check_eq(jsonBody.email, "796f75406578616d706c652e636f6d");
+ do_check_eq(jsonBody.password, "biggersecret");
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(creationMessage, creationMessage.length);
+ },
+ "/raw_password/session/create": function(request, response) {
+ let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ let jsonBody = JSON.parse(body);
+ if (jsonBody.password === "bigsecret") {
+ do_check_eq(jsonBody.email, "6dc3a9406578616d706c652e636f6d");
+ } else if (jsonBody.password === "biggersecret") {
+ do_check_eq(jsonBody.email, "796f75406578616d706c652e636f6d");
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
+ },
+ "/recovery_email/status": function(request, response) {
+ do_check_true(request.hasHeader("Authorization"));
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(emailStatus, emailStatus.length);
+ },
+ "/session/destroy": function(request, response) {
+ do_check_true(request.hasHeader("Authorization"));
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
+ },
+ "/certificate/sign": function(request, response) {
+ do_check_true(request.hasHeader("Authorization"));
+ let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ let jsonBody = JSON.parse(body);
+ do_check_eq(JSON.parse(jsonBody.publicKey).foo, "bar");
+ do_check_eq(jsonBody.duration, 600);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
+ },
+ "/auth/start": function(request, response) {
+ if (authStarts === 0) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ writeResp(response, JSON.stringify({}));
+ } else if (authStarts === 1) {
+ response.setStatusLine(request.httpVersion, 400, "NOT OK");
+ writeResp(response, JSON.stringify({errno: 102, error: "no such account"}));
+ } else if (authStarts === 2) {
+ response.setStatusLine(request.httpVersion, 400, "NOT OK");
+ writeResp(response, JSON.stringify({errno: 107, error: "boom"}));
+ }
+ authStarts++;
+ },
+ }
+ );
+
+ let client = new FxAccountsClient(server.baseURI);
+ let result = undefined;
+
+ result = yield client.signUp('you@example.com', 'biggersecret');
+ do_check_eq("NotARealUid", result.uid);
+
+ result = yield client.signIn('mé@example.com', 'bigsecret');
+ do_check_eq("NotARealToken", result.sessionToken);
+
+ result = yield client.signOut('NotARealToken');
+ do_check_eq(typeof result, "object");
+
+ result = yield client.recoveryEmailStatus('NotARealToken');
+ do_check_eq(result.verified, true);
+
+ result = yield client.signCertificate('NotARealToken', JSON.stringify({foo: "bar"}), 600);
+ do_check_eq("baz", result.bar);
+
+ result = yield client.accountExists('hey@example.com');
+ do_check_eq(result, true);
+ result = yield client.accountExists('hey2@example.com');
+ do_check_eq(result, false);
+ try {
+ result = yield client.accountExists('hey3@example.com');
+ } catch(e) {
+ do_check_eq(e.errno, 107);
+ }
+
+ yield deferredStop(server);
+});
+
+add_task(function test_error_response() {
+ let errorMessage = JSON.stringify({error: "Oops", code: 400, errno: 99});
+
+ let server = httpd_setup(
+ {
+ "/raw_password/session/create": function(request, response) {
+ let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+
+ response.setStatusLine(request.httpVersion, 400, "NOT OK");
+ response.bodyOutputStream.write(errorMessage, errorMessage.length);
+ },
+ }
+ );
+
+ let client = new FxAccountsClient(server.baseURI);
+
+ try {
+ let result = yield client.signIn('mé@example.com', 'bigsecret');
+ } catch(result) {
+ do_check_eq("Oops", result.error);
+ do_check_eq(400, result.code);
+ do_check_eq(99, result.errno);
+ }
+
+ yield deferredStop(server);
+});
diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.ini b/services/fxaccounts/tests/xpcshell/xpcshell.ini
new file mode 100644
index 00000000000..b69bc96575f
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
+tail =
+
+[test_client.js]
+
diff --git a/services/moz.build b/services/moz.build
index 880f3cec4d5..25a2d1ccb84 100644
--- a/services/moz.build
+++ b/services/moz.build
@@ -7,6 +7,7 @@
PARALLEL_DIRS += [
'common',
'crypto',
+ 'fxaccounts',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
diff --git a/toolkit/components/places/PlacesDBUtils.jsm b/toolkit/components/places/PlacesDBUtils.jsm
index 7890c4c4f39..d981e7ee5aa 100644
--- a/toolkit/components/places/PlacesDBUtils.jsm
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -883,7 +883,7 @@ this.PlacesDBUtils = {
query: "SELECT count(*) FROM moz_keywords " },
{ histogram: "PLACES_SORTED_BOOKMARKS_PERC",
- query: "SELECT ROUND(( "
+ query: "SELECT IFNULL(ROUND(( "
+ "SELECT count(*) FROM moz_bookmarks b "
+ "JOIN moz_bookmarks t ON t.id = b.parent "
+ "AND t.parent <> :tags_folder AND t.parent > :places_root "
@@ -893,10 +893,10 @@ this.PlacesDBUtils = {
+ "JOIN moz_bookmarks t ON t.id = b.parent "
+ "AND t.parent <> :tags_folder "
+ "WHERE b.type = :type_bookmark "
- + ")) " },
+ + ")), 0) " },
{ histogram: "PLACES_TAGGED_BOOKMARKS_PERC",
- query: "SELECT ROUND(( "
+ query: "SELECT IFNULL(ROUND(( "
+ "SELECT count(*) FROM moz_bookmarks b "
+ "JOIN moz_bookmarks t ON t.id = b.parent "
+ "AND t.parent = :tags_folder "
@@ -905,7 +905,7 @@ this.PlacesDBUtils = {
+ "JOIN moz_bookmarks t ON t.id = b.parent "
+ "AND t.parent <> :tags_folder "
+ "WHERE b.type = :type_bookmark "
- + ")) " },
+ + ")), 0) " },
{ histogram: "PLACES_DATABASE_FILESIZE_MB",
callback: function () {
@@ -970,13 +970,12 @@ this.PlacesDBUtils = {
if ("callback" in aProbe) {
value = aProbe.callback(value);
}
- if (isFinite(value)) {
- probeValues[aProbe.histogram] = value;
- Services.telemetry.getHistogramById(aProbe.histogram)
- .add(value);
- }
+ probeValues[aProbe.histogram] = value;
+ Services.telemetry.getHistogramById(aProbe.histogram).add(value);
} catch (ex) {
- Components.utils.reportError(ex);
+ Components.utils.reportError("Error adding value " + value +
+ " to histogram " + aProbe.histogram +
+ ": " + ex);
}
if (!outstandingProbes && aHealthReportCallback) {
diff --git a/toolkit/modules/WindowsPrefSync.jsm b/toolkit/modules/WindowsPrefSync.jsm
index e976b6011a3..ee0d211fb3a 100644
--- a/toolkit/modules/WindowsPrefSync.jsm
+++ b/toolkit/modules/WindowsPrefSync.jsm
@@ -71,6 +71,15 @@ this.WindowsPrefSync = {
"app.update.metro.enabled",
"browser.sessionstore.resume_session_once"],
+ /**
+ * Returns the base path where registry sync prefs are stored.
+ */
+ get prefRegistryPath() {
+ let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].
+ createInstance(Ci.nsIToolkitProfileService);
+ return PREF_BASE_KEY + profileService.selectedProfile.name + "\\";
+ },
+
/**
* The following preferences will be pushed to registry from Metro
* Firefox and pulled in from Desktop Firefox.
@@ -112,8 +121,7 @@ this.WindowsPrefSync = {
let prefValue = Services.prefs[prefFunc](aPrefName);
registry.create(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
- PREF_BASE_KEY + prefType,
- Ci.nsIWindowsRegKey.ACCESS_WRITE);
+ this.prefRegistryPath + prefType, Ci.nsIWindowsRegKey.ACCESS_WRITE);
// Always write as string, but the registry subfolder will determine
// how Metro interprets that string value.
registry.writeStringValue(aPrefName, prefValue);
@@ -131,7 +139,7 @@ this.WindowsPrefSync = {
function pullSharedPrefType(prefType, prefFunc) {
try {
registry.create(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
- PREF_BASE_KEY + prefType,
+ self.prefRegistryPath + prefType,
Ci.nsIWindowsRegKey.ACCESS_ALL);
for (let i = 0; i < registry.valueCount; i++) {
let prefName = registry.getValueName(i);
diff --git a/widget/android/AndroidBridge.cpp b/widget/android/AndroidBridge.cpp
index d67a3fcfa1e..7322006f529 100644
--- a/widget/android/AndroidBridge.cpp
+++ b/widget/android/AndroidBridge.cpp
@@ -63,8 +63,8 @@ jclass AndroidBridge::GetClassGlobalRef(JNIEnv* env, const char* className)
{
jobject classLocalRef = env->FindClass(className);
if (!classLocalRef) {
- ALOG(">>> FATAL JNI ERROR! FindClass(className=\"%s\") failed. Did "
- "ProGuard optimize away a non-public class?", className);
+ ALOG(">>> FATAL JNI ERROR! FindClass(className=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+ className);
env->ExceptionDescribe();
MOZ_CRASH();
}
@@ -85,8 +85,8 @@ jmethodID AndroidBridge::GetMethodID(JNIEnv* env, jclass jClass,
jmethodID methodID = env->GetMethodID(jClass, methodName, methodType);
if (!methodID) {
ALOG(">>> FATAL JNI ERROR! GetMethodID(methodName=\"%s\", "
- "methodType=\"%s\") failed. Did ProGuard optimize away a non-"
- "public method?", methodName, methodType);
+ "methodType=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+ methodName, methodType);
env->ExceptionDescribe();
MOZ_CRASH();
}
@@ -99,8 +99,8 @@ jmethodID AndroidBridge::GetStaticMethodID(JNIEnv* env, jclass jClass,
jmethodID methodID = env->GetStaticMethodID(jClass, methodName, methodType);
if (!methodID) {
ALOG(">>> FATAL JNI ERROR! GetStaticMethodID(methodName=\"%s\", "
- "methodType=\"%s\") failed. Did ProGuard optimize away a non-"
- "public method?", methodName, methodType);
+ "methodType=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+ methodName, methodType);
env->ExceptionDescribe();
MOZ_CRASH();
}
@@ -113,8 +113,8 @@ jfieldID AndroidBridge::GetFieldID(JNIEnv* env, jclass jClass,
jfieldID fieldID = env->GetFieldID(jClass, fieldName, fieldType);
if (!fieldID) {
ALOG(">>> FATAL JNI ERROR! GetFieldID(fieldName=\"%s\", "
- "fieldType=\"%s\") failed. Did ProGuard optimize away a non-"
- "public field?", fieldName, fieldType);
+ "fieldType=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+ fieldName, fieldType);
env->ExceptionDescribe();
MOZ_CRASH();
}
@@ -127,8 +127,8 @@ jfieldID AndroidBridge::GetStaticFieldID(JNIEnv* env, jclass jClass,
jfieldID fieldID = env->GetStaticFieldID(jClass, fieldName, fieldType);
if (!fieldID) {
ALOG(">>> FATAL JNI ERROR! GetStaticFieldID(fieldName=\"%s\", "
- "fieldType=\"%s\") failed. Did ProGuard optimize away a non-"
- "public field?", fieldName, fieldType);
+ "fieldType=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+ fieldName, fieldType);
env->ExceptionDescribe();
MOZ_CRASH();
}
diff --git a/widget/windows/winrt/FrameworkView.cpp b/widget/windows/winrt/FrameworkView.cpp
index 19a4d48c726..1c3cf270313 100644
--- a/widget/windows/winrt/FrameworkView.cpp
+++ b/widget/windows/winrt/FrameworkView.cpp
@@ -414,7 +414,7 @@ FrameworkView::OnWindowActivated(ICoreWindow* aSender, IWindowActivatedEventArgs
{
LogFunction();
if (mShuttingDown || !mWidget)
- return E_FAIL;
+ return S_OK;
CoreWindowActivationState state;
aArgs->get_WindowActivationState(&state);
mWinActiveState = !(state == CoreWindowActivationState::CoreWindowActivationState_Deactivated);
diff --git a/widget/windows/winrt/MetroInput.cpp b/widget/windows/winrt/MetroInput.cpp
index a36ede4afd6..4330df71ffd 100644
--- a/widget/windows/winrt/MetroInput.cpp
+++ b/widget/windows/winrt/MetroInput.cpp
@@ -496,6 +496,8 @@ MetroInput::OnPointerPressed(UI::Core::ICoreWindow* aSender,
mRecognizerWantsEvents = true;
mCancelable = true;
mCanceledIds.Clear();
+ } else {
+ mCancelable = false;
}
InitTouchEventTouchList(touchEvent);
@@ -1141,7 +1143,7 @@ MetroInput::DeliverNextQueuedTouchEvent()
// Test for chrome vs. content target. To do this we only use the first touch
// point since that will be the input batch target. Cache this for touch events
// since HitTestChrome has to send a dom event.
- if (mCancelable && event->message == NS_TOUCH_START && mTouches.Count() == 1) {
+ if (mCancelable && event->message == NS_TOUCH_START) {
nsRefPtr touch = event->touches[0];
LayoutDeviceIntPoint pt = LayoutDeviceIntPoint::FromUntyped(touch->mRefPoint);
bool apzIntersect = mWidget->ApzHitTest(mozilla::ScreenIntPoint(pt.x, pt.y));
@@ -1153,6 +1155,17 @@ MetroInput::DeliverNextQueuedTouchEvent()
if (mChromeHitTestCacheForTouch) {
DUMP_TOUCH_IDS("DOM(1)", event);
mWidget->DispatchEvent(event, status);
+ if (mCancelable) {
+ // Disable gesture based events (taps, swipes, rotation) if
+ // preventDefault is called on touchstart.
+ if (nsEventStatus_eConsumeNoDefault == status) {
+ mRecognizerWantsEvents = false;
+ mGestureRecognizer->CompleteGesture();
+ }
+ if (event->message == NS_TOUCH_MOVE) {
+ mCancelable = false;
+ }
+ }
return;
}