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; }