mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge fx-team to mozilla-central
This commit is contained in:
commit
8c14f61fb1
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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]),
|
||||
|
@ -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");
|
||||
|
@ -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() {
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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.
|
||||
|
@ -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");
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
@ -260,7 +260,7 @@ Desktop browser's sync prefs.
|
||||
<observes element="bcast_windowState" attribute="*"/>
|
||||
<observes element="bcast_urlbarState" attribute="*"/>
|
||||
<hbox id="toolbar-context-page" pack="end">
|
||||
<circularprogressindicator id="download-progress"
|
||||
<circularprogressindicator id="download-progress" class="appbar-primary"
|
||||
oncommand="MetroDownloadsView.onDownloadButton()"/>
|
||||
<toolbarbutton id="star-button" class="appbar-primary"
|
||||
type="checkbox"
|
||||
|
@ -741,16 +741,7 @@ documenttab[selected] .documenttab-selection {
|
||||
}
|
||||
|
||||
#download-progress {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
list-style-image: url(chrome://browser/skin/images/navbar-download.png);
|
||||
-moz-image-region: rect(0px, 40px, 40px, 0px);
|
||||
}
|
||||
#download-progress:hover {
|
||||
-moz-image-region: rect(0px, 80px, 40px, 40px);
|
||||
}
|
||||
#download-progress:active {
|
||||
-moz-image-region: rect(0px, 120px, 40px, 80px);
|
||||
}
|
||||
|
||||
#pin-button {
|
||||
|
@ -788,8 +788,8 @@ appbar toolbar[labelled] toolbarbutton > .toolbarbutton-text {
|
||||
|
||||
/* Sprites */
|
||||
|
||||
.appbar-primary > .toolbarbutton-icon,
|
||||
.appbar-secondary > .toolbarbutton-icon {
|
||||
.appbar-primary .toolbarbutton-icon,
|
||||
.appbar-secondary .toolbarbutton-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -956,6 +956,10 @@ html|*.urlbar-input:-moz-lwtheme::-moz-placeholder,
|
||||
color: GrayText;
|
||||
}
|
||||
|
||||
#search-container {
|
||||
min-width: calc(54px + 11ch);
|
||||
}
|
||||
|
||||
/* identity box */
|
||||
|
||||
#identity-box {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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 \
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
201
mobile/android/config/proguard.cfg
Normal file
201
mobile/android/config/proguard.cfg
Normal file
@ -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 <methods>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
public <init>(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 <init>(android.content.Context);
|
||||
public <init>(android.content.Context, android.util.AttributeSet);
|
||||
public <init>(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 <init>(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 <fields>;
|
||||
}
|
||||
|
||||
# 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 <methods>;
|
||||
}
|
||||
|
||||
#
|
||||
# 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 <methods>;
|
||||
}
|
||||
-keepclasseswithmembers class * {
|
||||
@org.mozilla.gecko.mozglue.JNITarget <fields>;
|
||||
}
|
||||
|
||||
# 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 <methods>;
|
||||
}
|
||||
-keepclasseswithmembers class * {
|
||||
@org.mozilla.gecko.mozglue.RobocopTarget <fields>;
|
||||
}
|
||||
|
||||
# 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 <methods>;
|
||||
}
|
||||
-keepclasseswithmembers class * {
|
||||
@org.mozilla.gecko.mozglue.WebRTCJNITarget <fields>;
|
||||
}
|
||||
|
||||
# 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 <methods>;
|
||||
}
|
||||
-keepclasseswithmembers class * {
|
||||
@org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI <fields>;
|
||||
}
|
||||
|
||||
-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.**
|
@ -9,6 +9,7 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <android/log.h>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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. <code>null</code> 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.
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
330
services/fxaccounts/FxAccountsClient.jsm
Normal file
330
services/fxaccounts/FxAccountsClient.jsm
Normal file
@ -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;
|
||||
},
|
||||
};
|
||||
|
8
services/fxaccounts/moz.build
Normal file
8
services/fxaccounts/moz.build
Normal file
@ -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']
|
7
services/fxaccounts/tests/moz.build
Normal file
7
services/fxaccounts/tests/moz.build
Normal file
@ -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']
|
18
services/fxaccounts/tests/xpcshell/head.js
Normal file
18
services/fxaccounts/tests/xpcshell/head.js
Normal file
@ -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);
|
||||
|
231
services/fxaccounts/tests/xpcshell/test_client.js
Normal file
231
services/fxaccounts/tests/xpcshell/test_client.js
Normal file
@ -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 = "<h1>Ooops!</h1>";
|
||||
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);
|
||||
});
|
6
services/fxaccounts/tests/xpcshell/xpcshell.ini
Normal file
6
services/fxaccounts/tests/xpcshell/xpcshell.ini
Normal file
@ -0,0 +1,6 @@
|
||||
[DEFAULT]
|
||||
head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
|
||||
tail =
|
||||
|
||||
[test_client.js]
|
||||
|
@ -7,6 +7,7 @@
|
||||
PARALLEL_DIRS += [
|
||||
'common',
|
||||
'crypto',
|
||||
'fxaccounts',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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> 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;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user