mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge m-c to fx-team
This commit is contained in:
commit
5757bf7456
@ -12,6 +12,7 @@ const { Cc, Ci, CC } = require('chrome');
|
||||
const options = require('@loader/options');
|
||||
const file = require('./io/file');
|
||||
const runtime = require("./system/runtime");
|
||||
var cfxArgs = require("@test/options");
|
||||
|
||||
const appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
|
||||
getService(Ci.nsIAppStartup);
|
||||
@ -69,12 +70,13 @@ exports.exit = function exit(code) {
|
||||
stream.write(status, status.length);
|
||||
stream.flush();
|
||||
stream.close();
|
||||
if (cfxArgs.parseable) {
|
||||
console.log('wrote to resultFile');
|
||||
}
|
||||
}
|
||||
|
||||
if (code == 0) {
|
||||
forcedExit = true;
|
||||
}
|
||||
appStartup.quit(code ? E_ATTEMPT : E_FORCE);
|
||||
forcedExit = true;
|
||||
appStartup.quit(E_FORCE);
|
||||
};
|
||||
|
||||
// Adapter for nodejs's stdout & stderr:
|
||||
|
@ -7,6 +7,7 @@ module.metadata = {
|
||||
"stability": "experimental"
|
||||
};
|
||||
|
||||
var { setTimeout } = require("../timers");
|
||||
var { exit, stdout } = require("../system");
|
||||
var cfxArgs = require("@test/options");
|
||||
|
||||
@ -19,9 +20,16 @@ function runTests(findAndRunTests) {
|
||||
stdout.write(tests.passed + " of " + total + " tests passed.\n");
|
||||
|
||||
if (tests.failed == 0) {
|
||||
if (tests.passed === 0)
|
||||
if (tests.passed === 0) {
|
||||
stdout.write("No tests were run\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
if (cfxArgs.parseable) {
|
||||
console.log('calling exit(0)');
|
||||
}
|
||||
exit(0);
|
||||
}, 0);
|
||||
} else {
|
||||
if (cfxArgs.verbose || cfxArgs.parseable)
|
||||
printFailedTests(tests, stdout.write);
|
||||
|
@ -731,6 +731,7 @@ def run_app(harness_root_dir, manifest_rdf, harness_options,
|
||||
if os.path.exists(resultfile):
|
||||
result = open(resultfile).read()
|
||||
if result:
|
||||
sys.stderr.write("resultfile contained " + "'" + result + "'\n")
|
||||
if result in ['OK', 'FAIL']:
|
||||
done = True
|
||||
else:
|
||||
@ -749,7 +750,9 @@ def run_app(harness_root_dir, manifest_rdf, harness_options,
|
||||
else:
|
||||
runner.wait(10)
|
||||
finally:
|
||||
sys.stderr.write("Done.\n")
|
||||
outf.close()
|
||||
sys.stderr.write("Clean the profile.\n")
|
||||
if profile:
|
||||
profile.cleanup()
|
||||
|
||||
|
@ -223,7 +223,7 @@ var ctrlTab = {
|
||||
if (!this._recentlyUsedTabs) {
|
||||
tabPreviews.init();
|
||||
|
||||
this._recentlyUsedTabs = [gBrowser.selectedTab];
|
||||
this._initRecentlyUsedTabs();
|
||||
this._init(true);
|
||||
}
|
||||
},
|
||||
@ -495,6 +495,9 @@ var ctrlTab = {
|
||||
|
||||
handleEvent: function ctrlTab_handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "SSWindowStateReady":
|
||||
this._initRecentlyUsedTabs();
|
||||
break;
|
||||
case "TabAttrModified":
|
||||
// tab attribute modified (e.g. label, crop, busy, image, selected)
|
||||
for (let i = this.previews.length - 1; i >= 0; i--) {
|
||||
@ -530,9 +533,17 @@ var ctrlTab = {
|
||||
}
|
||||
},
|
||||
|
||||
_initRecentlyUsedTabs: function () {
|
||||
this._recentlyUsedTabs =
|
||||
Array.filter(gBrowser.tabs, tab => !tab.closing)
|
||||
.sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed);
|
||||
},
|
||||
|
||||
_init: function ctrlTab__init(enable) {
|
||||
var toggleEventListener = enable ? "addEventListener" : "removeEventListener";
|
||||
|
||||
window[toggleEventListener]("SSWindowStateReady", this, false);
|
||||
|
||||
var tabContainer = gBrowser.tabContainer;
|
||||
tabContainer[toggleEventListener]("TabOpen", this, false);
|
||||
tabContainer[toggleEventListener]("TabAttrModified", this, false);
|
||||
|
@ -760,14 +760,6 @@ toolbarpaletteitem[place="palette"] > toolbarbutton[type="badged"] > .toolbarbut
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
toolbarbutton[sdk-button="true"] > .toolbarbutton-icon {
|
||||
max-height: 32px;
|
||||
}
|
||||
|
||||
toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
panelview > .social-panel-frame {
|
||||
width: auto;
|
||||
height: auto;
|
||||
|
@ -296,6 +296,7 @@ const PanelUI = {
|
||||
tempPanel.setAttribute("id", "customizationui-widget-panel");
|
||||
tempPanel.setAttribute("class", "cui-widget-panel");
|
||||
tempPanel.setAttribute("level", "top");
|
||||
tempPanel.setAttribute("context", "");
|
||||
document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
|
||||
// If the view has a footer, set a convenience class on the panel.
|
||||
tempPanel.classList.toggle("cui-widget-panelWithFooter",
|
||||
|
@ -170,7 +170,7 @@ let CustomizableUIInternal = {
|
||||
anchor: "PanelUI-menu-button",
|
||||
type: CustomizableUI.TYPE_MENU_PANEL,
|
||||
defaultPlacements: panelPlacements
|
||||
});
|
||||
}, true);
|
||||
PanelWideWidgetTracker.init();
|
||||
|
||||
this.registerArea(CustomizableUI.AREA_NAVBAR, {
|
||||
@ -185,16 +185,30 @@ let CustomizableUIInternal = {
|
||||
"downloads-button",
|
||||
"home-button",
|
||||
"social-share-button",
|
||||
]
|
||||
});
|
||||
],
|
||||
defaultCollapsed: false,
|
||||
}, true);
|
||||
#ifndef XP_MACOSX
|
||||
this.registerArea(CustomizableUI.AREA_MENUBAR, {
|
||||
legacy: true,
|
||||
type: CustomizableUI.TYPE_TOOLBAR,
|
||||
defaultPlacements: [
|
||||
"menubar-items",
|
||||
]
|
||||
});
|
||||
],
|
||||
get defaultCollapsed() {
|
||||
#ifdef MENUBAR_CAN_AUTOHIDE
|
||||
#if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT)
|
||||
return true;
|
||||
#else
|
||||
// This is duplicated logic from /browser/base/jar.mn
|
||||
// for win6BrowserOverlay.xul.
|
||||
return Services.appinfo.OS == "WINNT" &&
|
||||
Services.sysinfo.getProperty("version") != "5.1";
|
||||
#endif
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
#endif
|
||||
this.registerArea(CustomizableUI.AREA_TABSTRIP, {
|
||||
legacy: true,
|
||||
@ -204,21 +218,24 @@ let CustomizableUIInternal = {
|
||||
"new-tab-button",
|
||||
"alltabs-button",
|
||||
"tabs-closebutton",
|
||||
]
|
||||
});
|
||||
],
|
||||
defaultCollapsed: false,
|
||||
}, true);
|
||||
this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
|
||||
legacy: true,
|
||||
type: CustomizableUI.TYPE_TOOLBAR,
|
||||
defaultPlacements: [
|
||||
"personal-bookmarks",
|
||||
]
|
||||
});
|
||||
],
|
||||
defaultCollapsed: true,
|
||||
}, true);
|
||||
|
||||
this.registerArea(CustomizableUI.AREA_ADDONBAR, {
|
||||
type: CustomizableUI.TYPE_TOOLBAR,
|
||||
legacy: true,
|
||||
defaultPlacements: ["addonbar-closebutton", "status-bar"]
|
||||
});
|
||||
defaultPlacements: ["addonbar-closebutton", "status-bar"],
|
||||
defaultCollapsed: false,
|
||||
}, true);
|
||||
},
|
||||
|
||||
_defineBuiltInWidgets: function() {
|
||||
@ -254,7 +271,7 @@ let CustomizableUIInternal = {
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
registerArea: function(aName, aProperties) {
|
||||
registerArea: function(aName, aProperties, aInternalCaller) {
|
||||
if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
|
||||
throw new Error("Invalid area name");
|
||||
}
|
||||
@ -274,6 +291,16 @@ let CustomizableUIInternal = {
|
||||
if (!props.has("type")) {
|
||||
props.set("type", CustomizableUI.TYPE_TOOLBAR);
|
||||
}
|
||||
if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
|
||||
if (!aInternalCaller && props.has("defaultCollapsed")) {
|
||||
throw new Error("defaultCollapsed is only allowed for default toolbars.")
|
||||
}
|
||||
if (!props.has("defaultCollapsed")) {
|
||||
props.set("defaultCollapsed", true);
|
||||
}
|
||||
} else if (props.has("defaultCollapsed")) {
|
||||
throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
|
||||
}
|
||||
// Sanity check type:
|
||||
let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL];
|
||||
if (allTypes.indexOf(props.get("type")) == -1) {
|
||||
@ -2041,6 +2068,13 @@ let CustomizableUIInternal = {
|
||||
let placements = gPlacements.get(areaId);
|
||||
for (let areaNode of areaNodes) {
|
||||
this.buildArea(areaId, placements, areaNode);
|
||||
|
||||
let area = gAreas.get(areaId);
|
||||
if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
|
||||
let defaultCollapsed = area.get("defaultCollapsed");
|
||||
let win = areaNode.ownerDocument.defaultView;
|
||||
win.setToolbarVisibility(areaNode, !defaultCollapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
gResetting = false;
|
||||
@ -2163,6 +2197,16 @@ let CustomizableUIInternal = {
|
||||
return itemNode && removableOrDefault(itemNode || item);
|
||||
});
|
||||
}
|
||||
|
||||
if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
|
||||
let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
|
||||
let collapsed = container.getAttribute(attribute) == "true";
|
||||
let defaultCollapsed = props.get("defaultCollapsed");
|
||||
if (collapsed != defaultCollapsed) {
|
||||
LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") +
|
||||
"\nvs.\n" + defaultPlacements.join(","));
|
||||
@ -2360,6 +2404,8 @@ this.CustomizableUI = {
|
||||
* effect for toolbars.
|
||||
* - defaultPlacements: an array of widget IDs making up the
|
||||
* default contents of the area
|
||||
* - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies
|
||||
* if toolbar is collapsed by default (default to true)
|
||||
*/
|
||||
registerArea: function(aName, aProperties) {
|
||||
CustomizableUIInternal.registerArea(aName, aProperties);
|
||||
@ -2738,6 +2784,16 @@ this.CustomizableUI = {
|
||||
let area = gAreas.get(aArea);
|
||||
return area ? area.get("type") : null;
|
||||
},
|
||||
/**
|
||||
* Check if a toolbar is collapsed by default.
|
||||
*
|
||||
* @param aArea the ID of the area whose default-collapsed state you want to know.
|
||||
* @return `true` or `false` depending on the area, null if the area is unknown.
|
||||
*/
|
||||
isToolbarDefaultCollapsed: function(aArea) {
|
||||
let area = gAreas.get(aArea);
|
||||
return area ? area.get("defaultCollapsed") : null;
|
||||
},
|
||||
/**
|
||||
* Obtain the DOM node that is the customize target for an area in a
|
||||
* specific window.
|
||||
|
@ -794,6 +794,7 @@ const CustomizableWidgets = [{
|
||||
onWidgetRemoved: (aWidgetId, aPrevArea) => {
|
||||
if (aWidgetId != this.id)
|
||||
return;
|
||||
aNode.removeAttribute("disabled");
|
||||
if (aPrevArea == CustomizableUI.AREA_PANEL) {
|
||||
let panel = document.getElementById(kPanelId);
|
||||
panel.removeEventListener("popupshowing", updateButton);
|
||||
|
@ -189,6 +189,11 @@ CustomizeMode.prototype = {
|
||||
// Hide the palette before starting the transition for increased perf.
|
||||
this.visiblePalette.hidden = true;
|
||||
|
||||
// Disable the button-text fade-out mask
|
||||
// during the transition for increased perf.
|
||||
let panelContents = window.PanelUI.contents;
|
||||
panelContents.setAttribute("customize-transitioning", "true");
|
||||
|
||||
// Move the mainView in the panel to the holder so that we can see it
|
||||
// while customizing.
|
||||
let mainView = window.PanelUI.mainView;
|
||||
@ -253,6 +258,8 @@ CustomizeMode.prototype = {
|
||||
this._updateEmptyPaletteNotice();
|
||||
|
||||
this._handler.isEnteringCustomizeMode = false;
|
||||
panelContents.removeAttribute("customize-transitioning");
|
||||
|
||||
this.dispatchToolboxEvent("customizationready");
|
||||
if (!this._wantToBeInCustomizeMode) {
|
||||
this.exit();
|
||||
@ -300,6 +307,11 @@ CustomizeMode.prototype = {
|
||||
this.visiblePalette.hidden = true;
|
||||
this.paletteEmptyNotice.hidden = true;
|
||||
|
||||
// Disable the button-text fade-out mask
|
||||
// during the transition for increased perf.
|
||||
let panelContents = window.PanelUI.contents;
|
||||
panelContents.setAttribute("customize-transitioning", "true");
|
||||
|
||||
this._transitioning = true;
|
||||
|
||||
Task.spawn(function() {
|
||||
@ -353,6 +365,8 @@ CustomizeMode.prototype = {
|
||||
document.getElementById("PanelUI-help").removeAttribute("disabled");
|
||||
document.getElementById("PanelUI-quit").removeAttribute("disabled");
|
||||
|
||||
panelContents.removeAttribute("customize-transitioning");
|
||||
|
||||
// We need to set this._customizing to false before removing the tab
|
||||
// or the TabSelect event handler will think that we are exiting
|
||||
// customization mode for a second time.
|
||||
@ -852,6 +866,7 @@ CustomizeMode.prototype = {
|
||||
} else {
|
||||
toolbar.removeAttribute("customizing");
|
||||
}
|
||||
this._onUIChange();
|
||||
},
|
||||
|
||||
onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
|
||||
|
@ -12,6 +12,9 @@ EXTRA_JS_MODULES += [
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'):
|
||||
DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'):
|
||||
DEFINES['MENUBAR_CAN_AUTOHIDE'] = 1
|
||||
|
||||
EXTRA_PP_JS_MODULES += [
|
||||
'CustomizableUI.jsm',
|
||||
'CustomizableWidgets.jsm',
|
||||
|
@ -63,4 +63,5 @@ skip-if = os == "linux"
|
||||
[browser_948985_non_removable_defaultArea.js]
|
||||
[browser_952963_areaType_getter_no_area.js]
|
||||
[browser_956602_remove_special_widget.js]
|
||||
[browser_969661_character_encoding_navbar_disabled.js]
|
||||
[browser_panel_toggle.js]
|
||||
|
@ -39,11 +39,10 @@ add_task(function checkRegisteringAndUnregistering() {
|
||||
[/customizableui-special-spring\d+/,
|
||||
kButtonId,
|
||||
/customizableui-special-spring\d+/]);
|
||||
ok(CustomizableUI.inDefaultState, "With a new toolbar and default placements, " +
|
||||
"everything should still be in a default state.");
|
||||
ok(!CustomizableUI.inDefaultState, "With a new toolbar it is no longer in a default state.");
|
||||
removeCustomToolbars(); // Will call unregisterArea for us
|
||||
ok(CustomizableUI.inDefaultState, "When the toolbar is unregistered, " +
|
||||
"everything should still be in a default state.");
|
||||
"everything will return to the default state.");
|
||||
});
|
||||
|
||||
add_task(function asyncCleanup() {
|
||||
|
@ -6,9 +6,10 @@
|
||||
|
||||
// Adding, moving and removing items should update the relevant currentset attributes
|
||||
add_task(function() {
|
||||
ok(CustomizableUI.inDefaultState, "Should be in the default state when we start");
|
||||
let personalbar = document.getElementById(CustomizableUI.AREA_BOOKMARKS);
|
||||
setToolbarVisibility(personalbar, true);
|
||||
ok(CustomizableUI.inDefaultState, "Should be in the default state when we start");
|
||||
ok(!CustomizableUI.inDefaultState, "Making the bookmarks toolbar visible takes it out of the default state");
|
||||
|
||||
let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
|
||||
let personalbar = document.getElementById(CustomizableUI.AREA_BOOKMARKS);
|
||||
|
@ -4,14 +4,119 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
let bookmarksToolbar = document.getElementById("PersonalToolbar");
|
||||
let navbar = document.getElementById("nav-bar");
|
||||
let tabsToolbar = document.getElementById("TabsToolbar");
|
||||
|
||||
// Customization reset should restore visibility to default-visible toolbars.
|
||||
add_task(function() {
|
||||
let navbar = document.getElementById("nav-bar");
|
||||
is(navbar.collapsed, false, "Test should start with navbar visible");
|
||||
navbar.collapsed = true;
|
||||
setToolbarVisibility(navbar, false);
|
||||
is(navbar.collapsed, true, "navbar should be hidden now");
|
||||
|
||||
yield resetCustomization();
|
||||
|
||||
is(navbar.collapsed, false, "Customization reset should restore visibility to the navbar");
|
||||
});
|
||||
|
||||
// Customization reset should restore collapsed-state to default-collapsed toolbars.
|
||||
add_task(function() {
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state");
|
||||
|
||||
is(bookmarksToolbar.collapsed, true, "Test should start with bookmarks toolbar collapsed");
|
||||
is(bookmarksToolbar.getBoundingClientRect().height, 0, "bookmarksToolbar should have height=0");
|
||||
isnot(tabsToolbar.getBoundingClientRect().height, 0, "TabsToolbar should have non-zero height");
|
||||
is(navbar.collapsed, false, "The nav-bar should be shown by default");
|
||||
|
||||
setToolbarVisibility(bookmarksToolbar, true);
|
||||
setToolbarVisibility(navbar, false);
|
||||
isnot(bookmarksToolbar.getBoundingClientRect().height, 0, "bookmarksToolbar should be visible now");
|
||||
is(navbar.getBoundingClientRect().height, 1, "navbar should have a height=1 (due to border)");
|
||||
is(CustomizableUI.inDefaultState, false, "Should no longer be in default state");
|
||||
|
||||
yield startCustomizing();
|
||||
gCustomizeMode.reset();
|
||||
yield waitForCondition(function() !gCustomizeMode.resetting);
|
||||
yield endCustomizing();
|
||||
|
||||
is(bookmarksToolbar.collapsed, true, "Customization reset should restore collapsed-state to the bookmarks toolbar");
|
||||
isnot(tabsToolbar.getBoundingClientRect().height, 0, "TabsToolbar should have non-zero height");
|
||||
is(bookmarksToolbar.getBoundingClientRect().height, 0, "The bookmarksToolbar should have height=0 after reset");
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be back to default state");
|
||||
});
|
||||
|
||||
// Check that the menubar will be collapsed by resetting, if the platform supports it.
|
||||
add_task(function() {
|
||||
let menubar = document.getElementById("toolbar-menubar");
|
||||
const canMenubarCollapse = CustomizableUI.isToolbarDefaultCollapsed(menubar.id);
|
||||
if (!canMenubarCollapse) {
|
||||
return;
|
||||
}
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state");
|
||||
|
||||
is(menubar.getBoundingClientRect().height, 0, "menubar should be hidden by default");
|
||||
setToolbarVisibility(menubar, true);
|
||||
isnot(menubar.getBoundingClientRect().height, 0, "menubar should be visible now");
|
||||
|
||||
yield startCustomizing();
|
||||
gCustomizeMode.reset();
|
||||
yield waitForCondition(function() !gCustomizeMode.resetting);
|
||||
|
||||
is(menubar.getAttribute("autohide"), "true", "The menubar should have autohide=true after reset in customization mode");
|
||||
is(menubar.getBoundingClientRect().height, 0, "The menubar should have height=0 after reset in customization mode");
|
||||
|
||||
yield endCustomizing();
|
||||
|
||||
is(menubar.getAttribute("autohide"), "true", "The menubar should have autohide=true after reset");
|
||||
is(menubar.getBoundingClientRect().height, 0, "The menubar should have height=0 after reset");
|
||||
});
|
||||
|
||||
// Customization reset should restore collapsed-state to default-collapsed toolbars.
|
||||
add_task(function() {
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state");
|
||||
is(bookmarksToolbar.getBoundingClientRect().height, 0, "bookmarksToolbar should have height=0");
|
||||
isnot(tabsToolbar.getBoundingClientRect().height, 0, "TabsToolbar should have non-zero height");
|
||||
|
||||
setToolbarVisibility(bookmarksToolbar, true);
|
||||
isnot(bookmarksToolbar.getBoundingClientRect().height, 0, "bookmarksToolbar should be visible now");
|
||||
is(CustomizableUI.inDefaultState, false, "Should no longer be in default state");
|
||||
|
||||
yield startCustomizing();
|
||||
|
||||
isnot(bookmarksToolbar.getBoundingClientRect().height, 0, "The bookmarksToolbar should be visible before reset");
|
||||
isnot(navbar.getBoundingClientRect().height, 0, "The navbar should be visible before reset");
|
||||
isnot(tabsToolbar.getBoundingClientRect().height, 0, "TabsToolbar should have non-zero height");
|
||||
|
||||
gCustomizeMode.reset();
|
||||
yield waitForCondition(function() !gCustomizeMode.resetting);
|
||||
|
||||
is(bookmarksToolbar.getBoundingClientRect().height, 0, "The bookmarksToolbar should have height=0 after reset");
|
||||
isnot(tabsToolbar.getBoundingClientRect().height, 0, "TabsToolbar should have non-zero height");
|
||||
isnot(navbar.getBoundingClientRect().height, 0, "The navbar should still be visible after reset");
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be back to default state");
|
||||
yield endCustomizing();
|
||||
});
|
||||
|
||||
// Check that the menubar will be collapsed by resetting, if the platform supports it.
|
||||
add_task(function() {
|
||||
let menubar = document.getElementById("toolbar-menubar");
|
||||
const canMenubarCollapse = CustomizableUI.isToolbarDefaultCollapsed(menubar.id);
|
||||
if (!canMenubarCollapse) {
|
||||
return;
|
||||
}
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state");
|
||||
yield startCustomizing();
|
||||
let resetButton = document.getElementById("customization-reset-button");
|
||||
is(resetButton.disabled, true, "The reset button should be disabled when in default state");
|
||||
|
||||
setToolbarVisibility(menubar, true);
|
||||
is(resetButton.disabled, false, "The reset button should be enabled when not in default state")
|
||||
ok(!CustomizableUI.inDefaultState, "No longer in default state when the menubar is shown");
|
||||
|
||||
yield gCustomizeMode.reset();
|
||||
|
||||
is(resetButton.disabled, true, "The reset button should be disabled when in default state");
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state");
|
||||
|
||||
yield endCustomizing();
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ registerCleanupFunction(cleanup);
|
||||
|
||||
// Registering a toolbar with defaultset attribute should work
|
||||
add_task(function() {
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state.");
|
||||
let btn = createDummyXULButton(kButtonId);
|
||||
let toolbar = document.createElement("toolbar");
|
||||
toolbar.id = kToolbarId;
|
||||
@ -21,7 +22,7 @@ add_task(function() {
|
||||
is(CustomizableUI.getAreaType(kToolbarId), CustomizableUI.TYPE_TOOLBAR,
|
||||
"Area should be registered as toolbar");
|
||||
assertAreaPlacements(kToolbarId, [kButtonId]);
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state.");
|
||||
ok(!CustomizableUI.inDefaultState, "No longer in default state after toolbar is registered and visible.");
|
||||
CustomizableUI.unregisterArea(kToolbarId, true);
|
||||
toolbar.remove();
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state.");
|
||||
@ -31,6 +32,7 @@ add_task(function() {
|
||||
// Registering a toolbar without a defaultset attribute should
|
||||
// wait for the registerArea call
|
||||
add_task(function() {
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state.");
|
||||
let btn = createDummyXULButton(kButtonId);
|
||||
let toolbar = document.createElement("toolbar");
|
||||
toolbar.id = kToolbarId;
|
||||
@ -44,7 +46,7 @@ add_task(function() {
|
||||
is(CustomizableUI.getAreaType(kToolbarId), CustomizableUI.TYPE_TOOLBAR,
|
||||
"Area should be registered as toolbar");
|
||||
assertAreaPlacements(kToolbarId, [kButtonId]);
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state.");
|
||||
ok(!CustomizableUI.inDefaultState, "No longer in default state after toolbar is registered and visible.");
|
||||
CustomizableUI.unregisterArea(kToolbarId, true);
|
||||
toolbar.remove();
|
||||
ok(CustomizableUI.inDefaultState, "Everything should be in its default state.");
|
||||
|
@ -0,0 +1,26 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
// Adding the character encoding menu to the panel, exiting customize mode,
|
||||
// and moving it to the nav-bar should have it enabled, not disabled.
|
||||
add_task(function() {
|
||||
yield startCustomizing();
|
||||
CustomizableUI.addWidgetToArea("characterencoding-button", "PanelUI-contents");
|
||||
yield endCustomizing();
|
||||
yield PanelUI.show();
|
||||
let panelHiddenPromise = promisePanelHidden(window);
|
||||
PanelUI.hide();
|
||||
yield panelHiddenPromise;
|
||||
CustomizableUI.addWidgetToArea("characterencoding-button", 'nav-bar');
|
||||
let button = document.getElementById("characterencoding-button");
|
||||
ok(!button.hasAttribute("disabled"), "Button shouldn't be disabled");
|
||||
});
|
||||
|
||||
add_task(function asyncCleanup() {
|
||||
resetCustomization();
|
||||
});
|
||||
|
@ -2521,6 +2521,10 @@ let SessionStoreInternal = {
|
||||
else
|
||||
tabbrowser.showTab(tab);
|
||||
|
||||
if (tabData.lastAccessed) {
|
||||
tab.lastAccessed = tabData.lastAccessed;
|
||||
}
|
||||
|
||||
if ("attributes" in tabData) {
|
||||
// Ensure that we persist tab attributes restored from previous sessions.
|
||||
Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a));
|
||||
|
@ -457,7 +457,10 @@ let gDevToolsBrowser = {
|
||||
let toolbox = gDevTools.getToolbox(target);
|
||||
let toolDefinition = gDevTools.getToolDefinition(toolId);
|
||||
|
||||
if (toolbox && toolbox.currentToolId == toolId) {
|
||||
if (toolbox &&
|
||||
(toolbox.currentToolId == toolId ||
|
||||
(toolId == "webconsole" && toolbox.splitConsole)))
|
||||
{
|
||||
toolbox.fireCustomKey(toolId);
|
||||
|
||||
if (toolDefinition.preventClosingOnKey || toolbox.hostType == devtools.Toolbox.HostType.WINDOW) {
|
||||
|
@ -467,8 +467,10 @@ Toolbox.prototype = {
|
||||
fireCustomKey: function(toolId) {
|
||||
let toolDefinition = gDevTools.getToolDefinition(toolId);
|
||||
|
||||
if (toolDefinition.onkey && this.currentToolId === toolId) {
|
||||
toolDefinition.onkey(this.getCurrentPanel());
|
||||
if (toolDefinition.onkey &&
|
||||
((this.currentToolId === toolId) ||
|
||||
(toolId == "webconsole" && this.splitConsole))) {
|
||||
toolDefinition.onkey(this.getCurrentPanel(), this);
|
||||
}
|
||||
},
|
||||
|
||||
@ -817,6 +819,14 @@ Toolbox.prototype = {
|
||||
iframe.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus split console's input line
|
||||
*/
|
||||
focusConsoleInput: function() {
|
||||
let hud = this.getPanel("webconsole").hud;
|
||||
hud.jsterm.inputNode.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the split state of the webconsole. If the webconsole panel
|
||||
* is already selected, then this command is ignored.
|
||||
@ -832,7 +842,7 @@ Toolbox.prototype = {
|
||||
|
||||
if (this._splitConsole) {
|
||||
this.loadTool("webconsole").then(() => {
|
||||
this.focusTool("webconsole");
|
||||
this.focusConsoleInput();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,14 @@ Tools.webConsole = {
|
||||
tooltip: l10n("ToolboxWebconsole.tooltip", webConsoleStrings),
|
||||
inMenu: true,
|
||||
|
||||
preventClosingOnKey: true,
|
||||
onkey: function(panel, toolbox) {
|
||||
if (toolbox.splitConsole)
|
||||
return toolbox.focusConsoleInput();
|
||||
|
||||
panel.focusInput();
|
||||
},
|
||||
|
||||
isTargetSupported: function(target) {
|
||||
return true;
|
||||
},
|
||||
|
@ -6,9 +6,11 @@
|
||||
|
||||
const {Cc, Ci, Cu} = require("chrome");
|
||||
|
||||
loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
|
||||
loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
|
||||
loader.lazyGetter(this, "HUDService", () => require("devtools/webconsole/hudservice"));
|
||||
loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter"));
|
||||
loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
|
||||
|
||||
/**
|
||||
* A DevToolPanel that controls the Web Console.
|
||||
@ -19,11 +21,27 @@ function WebConsolePanel(iframeWindow, toolbox)
|
||||
this._toolbox = toolbox;
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
exports.WebConsolePanel = WebConsolePanel;
|
||||
|
||||
WebConsolePanel.prototype = {
|
||||
hud: null,
|
||||
|
||||
/**
|
||||
* Called by the WebConsole's onkey command handler.
|
||||
* If the WebConsole is opened, check if the JSTerm's input line has focus.
|
||||
* If not, focus it.
|
||||
*/
|
||||
focusInput: function WCP_focusInput()
|
||||
{
|
||||
let inputNode = this.hud.jsterm.inputNode;
|
||||
|
||||
if (!inputNode.getAttribute("focused"))
|
||||
{
|
||||
inputNode.focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open is effectively an asynchronous constructor.
|
||||
*
|
||||
|
@ -36,9 +36,9 @@ function testClosingAfterCompletion(hud) {
|
||||
});
|
||||
|
||||
if (Services.appinfo.OS == "Darwin") {
|
||||
EventUtils.synthesizeKey("k", { accelKey: true, altKey: true });
|
||||
EventUtils.synthesizeKey("i", { accelKey: true, altKey: true });
|
||||
} else {
|
||||
EventUtils.synthesizeKey("k", { accelKey: true, shiftKey: true });
|
||||
EventUtils.synthesizeKey("i", { accelKey: true, shiftKey: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -562,7 +562,6 @@ WebConsoleFrame.prototype = {
|
||||
|
||||
this.jsterm = new JSTerm(this);
|
||||
this.jsterm.init();
|
||||
this.jsterm.inputNode.focus();
|
||||
|
||||
let toolbox = gDevTools.getToolbox(this.owner.target);
|
||||
if (toolbox) {
|
||||
@ -576,8 +575,9 @@ WebConsoleFrame.prototype = {
|
||||
*/
|
||||
this._addFocusCallback(this.outputNode, (evt) => {
|
||||
if ((evt.target.nodeName.toLowerCase() != "a") &&
|
||||
(evt.target.parentNode.nodeName.toLowerCase() != "a"))
|
||||
(evt.target.parentNode.nodeName.toLowerCase() != "a")) {
|
||||
this.jsterm.inputNode.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle the timestamp on preference change
|
||||
@ -586,14 +586,17 @@ WebConsoleFrame.prototype = {
|
||||
pref: PREF_MESSAGE_TIMESTAMP,
|
||||
newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP),
|
||||
});
|
||||
|
||||
// focus input node
|
||||
this.jsterm.inputNode.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the focus to JavaScript input field when the web console tab is
|
||||
* selected.
|
||||
* selected or when there is a split console present.
|
||||
* @private
|
||||
*/
|
||||
_onPanelSelected: function WCF__onPanelSelected()
|
||||
_onPanelSelected: function WCF__onPanelSelected(evt, id)
|
||||
{
|
||||
this.jsterm.inputNode.focus();
|
||||
},
|
||||
|
@ -67,8 +67,6 @@ var ContextCommands = {
|
||||
// content
|
||||
if (ContextMenuUI.popupState.string) {
|
||||
this.sendCommand("copy");
|
||||
|
||||
SelectionHelperUI.closeEditSession(true);
|
||||
}
|
||||
} else if (ContextMenuUI.popupState.string) {
|
||||
this.clipboard.copyString(ContextMenuUI.popupState.string, this.docRef);
|
||||
|
@ -911,6 +911,48 @@ gTests.push({
|
||||
}
|
||||
});
|
||||
|
||||
gTests.push({
|
||||
desc: "Bug 867499 - Selecting 'copy' from context menu for selected text " +
|
||||
"dismisses selection.",
|
||||
run: function test() {
|
||||
info(chromeRoot + "browser_context_menu_tests_02.html");
|
||||
yield addTab(chromeRoot + "browser_context_menu_tests_02.html");
|
||||
|
||||
emptyClipboard();
|
||||
ContextUI.dismiss();
|
||||
|
||||
yield waitForCondition(() => !ContextUI.navbarVisible);
|
||||
|
||||
let tabWindow = Browser.selectedTab.browser.contentWindow;
|
||||
let testSpan = tabWindow.document.getElementById("text1");
|
||||
|
||||
let promise = waitForEvent(document, "popupshown");
|
||||
sendContextMenuClickToElement(tabWindow, testSpan, 5, 5);
|
||||
yield promise;
|
||||
|
||||
yield waitForCondition(()=>SelectionHelperUI.isSelectionUIVisible);
|
||||
|
||||
promise = waitForEvent(document, "popupshown");
|
||||
sendContextMenuClickToSelection(tabWindow);
|
||||
yield promise;
|
||||
|
||||
let copyMenuItem = document.getElementById("context-copy");
|
||||
|
||||
ok(!copyMenuItem.hidden, "Copy menu item should be visible.");
|
||||
|
||||
promise = waitForEvent(document, "popuphidden");
|
||||
sendNativeTap(copyMenuItem, 5, 5);
|
||||
yield promise;
|
||||
yield waitForCondition(() =>
|
||||
!!SpecialPowers.getClipboardData("text/unicode"));
|
||||
|
||||
ok(SelectionHelperUI.isSelectionUIVisible,
|
||||
"Selection monocles should stay active after copy action.");
|
||||
|
||||
Browser.closeTab(Browser.selectedTab, { forceClose: true });
|
||||
}
|
||||
});
|
||||
|
||||
function test() {
|
||||
setDevPixelEqualToPx();
|
||||
runTests();
|
||||
|
@ -422,6 +422,7 @@ this.UITour = {
|
||||
this.hideHighlight(aWindow);
|
||||
this.hideInfo(aWindow);
|
||||
aWindow.PanelUI.panel.removeAttribute("noautohide");
|
||||
this.recreatePopup(aWindow.PanelUI.panel);
|
||||
}
|
||||
|
||||
this.endUrlbarCapture(aWindow);
|
||||
@ -822,6 +823,10 @@ this.UITour = {
|
||||
|
||||
if (aMenuName == "appMenu") {
|
||||
aWindow.PanelUI.panel.setAttribute("noautohide", "true");
|
||||
// If the popup is already opened, don't recreate the widget as it may cause a flicker.
|
||||
if (aWindow.PanelUI.panel.state != "open") {
|
||||
this.recreatePopup(aWindow.PanelUI.panel);
|
||||
}
|
||||
aWindow.PanelUI.panel.addEventListener("popuphiding", this.hidePanelAnnotations);
|
||||
aWindow.PanelUI.panel.addEventListener("ViewShowing", this.hidePanelAnnotations);
|
||||
if (aOpenCallback) {
|
||||
@ -843,6 +848,7 @@ this.UITour = {
|
||||
if (aMenuName == "appMenu") {
|
||||
aWindow.PanelUI.panel.removeAttribute("noautohide");
|
||||
aWindow.PanelUI.hide();
|
||||
this.recreatePopup(aWindow.PanelUI.panel);
|
||||
} else if (aMenuName == "bookmarks") {
|
||||
closeMenuButton("bookmarks-menu-button");
|
||||
}
|
||||
@ -873,6 +879,14 @@ this.UITour = {
|
||||
UITour.appMenuOpenForAnnotation.clear();
|
||||
},
|
||||
|
||||
recreatePopup: function(aPanel) {
|
||||
// After changing popup attributes that relate to how the native widget is created
|
||||
// (e.g. @noautohide) we need to re-create the frame/widget for it to take effect.
|
||||
aPanel.hidden = true;
|
||||
aPanel.clientWidth; // flush
|
||||
aPanel.hidden = false;
|
||||
},
|
||||
|
||||
startUrlbarCapture: function(aWindow, aExpectedText, aUrl) {
|
||||
let urlbar = aWindow.document.getElementById("urlbar");
|
||||
this.urlbarCapture.set(aWindow, {
|
||||
|
@ -557,13 +557,29 @@ menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip) {
|
||||
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon,
|
||||
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-icon {
|
||||
-moz-margin-end: 0;
|
||||
padding: 3px 7px;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
transition-property: background-color, border-color;
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
|
||||
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-icon,
|
||||
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
/* Help SDK icons fit: */
|
||||
toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
:-moz-any(#TabsToolbar, #nav-bar) toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
/* XXXgijs box models strike again: this is 16px + 2 * 7px padding + 2 * 1px border (from the rules above) */
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
|
||||
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-button:not([disabled]):hover > .toolbarbutton-icon,
|
||||
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not([buttonover]):not([open]):hover > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon,
|
||||
:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not([disabled]):hover > .toolbarbutton-icon {
|
||||
|
@ -1190,11 +1190,17 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button,
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-icon,
|
||||
.toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
|
||||
/* Help 16px icons fit: */
|
||||
.toolbarbutton-1[cui-areatype="toolbar"]:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-icon,
|
||||
.toolbarbutton-1[cui-areatype="toolbar"] > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* Help SDK icons fit: */
|
||||
toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1[disabled="true"] > .toolbarbutton-badge-container > .toolbarbutton-icon,
|
||||
#main-window:not([customizing]) .toolbarbutton-1 > .toolbarbutton-menubutton-button[disabled="true"] > .toolbarbutton-icon,
|
||||
|
@ -18,9 +18,9 @@
|
||||
%include ../browser.inc
|
||||
|
||||
#PanelUI-button {
|
||||
background-image: -moz-linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0)),
|
||||
-moz-linear-gradient(hsla(210,54%,20%,0), hsla(210,54%,20%,.3) 30%, hsla(210,54%,20%,.3) 70%, hsla(210,54%,20%,0)),
|
||||
-moz-linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0));
|
||||
background-image: linear-gradient(to bottom, hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0)),
|
||||
linear-gradient(to bottom, hsla(210,54%,20%,0), hsla(210,54%,20%,.3) 30%, hsla(210,54%,20%,.3) 70%, hsla(210,54%,20%,0)),
|
||||
linear-gradient(to bottom, hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0));
|
||||
background-size: 1px calc(100% - 1px), 1px calc(100% - 1px), 1px calc(100% - 1px) !important;
|
||||
background-position: 0px 0px, 1px 0px, 2px 0px;
|
||||
background-repeat: no-repeat;
|
||||
@ -104,7 +104,6 @@
|
||||
|
||||
#PanelUI-popup > .panel-arrowcontainer > .panel-arrowcontent {
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#PanelUI-popup > .panel-arrowcontainer > .panel-arrowcontent,
|
||||
@ -116,10 +115,14 @@
|
||||
.panelUI-grid .toolbarbutton-1 > .toolbarbutton-multiline-text {
|
||||
text-align: center;
|
||||
-moz-hyphens: auto;
|
||||
mask: url(chrome://browser/content/browser.xul#menuPanelButtonTextFadeOutMask);
|
||||
min-height: 3.5em;
|
||||
}
|
||||
|
||||
.panelUI-grid:not([customize-transitioning]) .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text,
|
||||
.panelUI-grid:not([customize-transitioning]) .toolbarbutton-1 > .toolbarbutton-multiline-text {
|
||||
mask: url(chrome://browser/content/browser.xul#menuPanelButtonTextFadeOutMask);
|
||||
}
|
||||
|
||||
.panelUI-grid .toolbarbutton-1 > .toolbarbutton-multiline-text {
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
@ -187,6 +190,13 @@ toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"]:not(.panel-wide-it
|
||||
height: calc(40px + 4em);
|
||||
}
|
||||
|
||||
/* Help SDK buttons fit in. */
|
||||
toolbarpaletteitem[place="palette"] > #personal-bookmarks > #bookmarks-toolbar-placeholder,
|
||||
#personal-bookmarks[cui-areatype="menu-panel"] > #bookmarks-toolbar-placeholder {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.customization-palette .toolbarbutton-1 {
|
||||
-moz-appearance: none;
|
||||
-moz-box-orient: vertical;
|
||||
|
@ -181,28 +181,48 @@ box.requests-menu-status {
|
||||
background-color: rgba(44, 187, 15, 1); /* green */
|
||||
}
|
||||
|
||||
/* 3xx are triangles */
|
||||
.theme-dark box.requests-menu-status[code^="3"] {
|
||||
background-color: rgba(94, 136, 176, 1); /* grey */
|
||||
background-color: transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 10px solid rgba(217, 155, 40, 1); /* light orange */
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.theme-light box.requests-menu-status[code^="3"] {
|
||||
background-color: rgba(95, 136, 176, 1); /* blue grey */
|
||||
background-color: transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-bottom: 10px solid rgba(217, 126, 0, 1); /* light orange */
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 4xx and 5xx are squares - error codes */
|
||||
.theme-dark box.requests-menu-status[code^="4"] {
|
||||
background-color: rgba(235, 83, 104, 1); /* red */
|
||||
border-radius: 0; /* squares */
|
||||
}
|
||||
|
||||
.theme-light box.requests-menu-status[code^="4"] {
|
||||
background-color: rgba(237, 38, 85, 1); /* red */
|
||||
border-radius: 0; /* squares */
|
||||
}
|
||||
|
||||
.theme-dark box.requests-menu-status[code^="5"] {
|
||||
background-color: rgba(223, 128, 255, 1); /* pink? */
|
||||
border-radius: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.theme-light box.requests-menu-status[code^="5"] {
|
||||
background-color: rgba(184, 46, 229, 1); /* pink! */
|
||||
border-radius: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* Network requests table: waterfall header */
|
||||
|
@ -537,6 +537,16 @@ menuitem.bookmark-item {
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
/* Help SDK icons fit: */
|
||||
toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
#nav-bar toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
|
||||
/* XXXgijs box models strike again: this is 16px + 2 * 7px padding + 2 * 1px border (from the rules above) */
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
#nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#social-provider-button):not(#PanelUI-menu-button) > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1[type=menu] > .toolbarbutton-text /* hack for add-ons that forcefully display the label */ {
|
||||
-moz-padding-end: 17px;
|
||||
|
@ -136,9 +136,8 @@ function cacheSnippets(response) {
|
||||
function loadSnippetsFromCache() {
|
||||
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) {
|
||||
update();
|
||||
Cu.reportError("Couldn't show snippets because cache does not exist yet.");
|
||||
} else {
|
||||
Cu.reportError("Error loading snippets from cache: " + e);
|
||||
}
|
||||
|
@ -5,6 +5,24 @@
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
// A wise line of Greek verse, and the utf-8 byte encoding.
|
||||
// N.b., Greek begins at utf-8 ce 91
|
||||
const TEST_STR = "πόλλ' οἶδ' ἀλώπηξ, ἀλλ' ἐχῖνος ἓν μέγα";
|
||||
const TEST_HEX = h("cf 80 cf 8c ce bb ce bb 27 20 ce bf e1 bc b6 ce"+
|
||||
"b4 27 20 e1 bc 80 ce bb cf 8e cf 80 ce b7 ce be"+
|
||||
"2c 20 e1 bc 80 ce bb ce bb 27 20 e1 bc 90 cf 87"+
|
||||
"e1 bf 96 ce bd ce bf cf 82 20 e1 bc 93 ce bd 20"+
|
||||
"ce bc ce ad ce b3 ce b1");
|
||||
// Integer byte values for the above
|
||||
const TEST_BYTES = [207,128,207,140,206,187,206,187,
|
||||
39, 32,206,191,225,188,182,206,
|
||||
180, 39, 32,225,188,128,206,187,
|
||||
207,142,207,128,206,183,206,190,
|
||||
44, 32,225,188,128,206,187,206,
|
||||
187, 39, 32,225,188,144,207,135,
|
||||
225,191,150,206,189,206,191,207,
|
||||
130, 32,225,188,147,206,189, 32,
|
||||
206,188,206,173,206,179,206,177];
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
@ -53,3 +71,70 @@ add_test(function test_bad_argument() {
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_stringAsHex() {
|
||||
do_check_eq(TEST_HEX, CommonUtils.stringAsHex(TEST_STR));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_hexAsString() {
|
||||
do_check_eq(TEST_STR, CommonUtils.hexAsString(TEST_HEX));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_hexToBytes() {
|
||||
let bytes = CommonUtils.hexToBytes(TEST_HEX);
|
||||
do_check_eq(TEST_BYTES.length, bytes.length);
|
||||
// Ensure that the decimal values of each byte are correct
|
||||
do_check_true(arraysEqual(TEST_BYTES,
|
||||
CommonUtils.stringToByteArray(bytes)));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_bytesToHex() {
|
||||
// Create a list of our character bytes from the reference int values
|
||||
let bytes = CommonUtils.byteArrayToString(TEST_BYTES);
|
||||
do_check_eq(TEST_HEX, CommonUtils.bytesAsHex(bytes));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_stringToBytes() {
|
||||
do_check_true(arraysEqual(TEST_BYTES,
|
||||
CommonUtils.stringToByteArray(CommonUtils.stringToBytes(TEST_STR))));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_stringRoundTrip() {
|
||||
do_check_eq(TEST_STR,
|
||||
CommonUtils.hexAsString(CommonUtils.stringAsHex(TEST_STR)));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_hexRoundTrip() {
|
||||
do_check_eq(TEST_HEX,
|
||||
CommonUtils.stringAsHex(CommonUtils.hexAsString(TEST_HEX)));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_byteArrayRoundTrip() {
|
||||
do_check_true(arraysEqual(TEST_BYTES,
|
||||
CommonUtils.stringToByteArray(CommonUtils.byteArrayToString(TEST_BYTES))));
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// turn formatted test vectors into normal hex strings
|
||||
function h(hexStr) {
|
||||
return hexStr.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
function arraysEqual(a1, a2) {
|
||||
if (a1.length !== a2.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < a1.length; i++) {
|
||||
if (a1[i] !== a2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -196,12 +196,21 @@ this.CommonUtils = {
|
||||
return [String.fromCharCode(byte) for each (byte in bytes)].join("");
|
||||
},
|
||||
|
||||
stringToByteArray: function stringToByteArray(bytesString) {
|
||||
return [String.charCodeAt(byte) for each (byte in bytesString)];
|
||||
},
|
||||
|
||||
bytesAsHex: function bytesAsHex(bytes) {
|
||||
let hex = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
hex += ("0" + bytes[i].charCodeAt().toString(16)).slice(-2);
|
||||
}
|
||||
return hex;
|
||||
return [("0" + bytes.charCodeAt(byte).toString(16)).slice(-2)
|
||||
for (byte in bytes)].join("");
|
||||
},
|
||||
|
||||
stringAsHex: function stringAsHex(str) {
|
||||
return CommonUtils.bytesAsHex(CommonUtils.encodeUTF8(str));
|
||||
},
|
||||
|
||||
stringToBytes: function stringToBytes(str) {
|
||||
return CommonUtils.hexToBytes(CommonUtils.stringAsHex(str));
|
||||
},
|
||||
|
||||
hexToBytes: function hexToBytes(str) {
|
||||
@ -212,6 +221,10 @@ this.CommonUtils = {
|
||||
return String.fromCharCode.apply(String, bytes);
|
||||
},
|
||||
|
||||
hexAsString: function hexAsString(hex) {
|
||||
return CommonUtils.decodeUTF8(CommonUtils.hexToBytes(hex));
|
||||
},
|
||||
|
||||
/**
|
||||
* Base32 encode (RFC 4648) a string
|
||||
*/
|
||||
|
@ -167,20 +167,24 @@ this.CryptoUtils = {
|
||||
* c: the number of iterations, a positive integer: e.g., 4096
|
||||
* dkLen: the length in octets of the destination
|
||||
* key, a positive integer: e.g., 16
|
||||
* hmacAlg: The algorithm to use for hmac
|
||||
* hmacLen: The hmac length
|
||||
*
|
||||
* The default value of 20 for hmacLen is appropriate for SHA1. For SHA256,
|
||||
* hmacLen should be 32.
|
||||
*
|
||||
* The output is an octet string of length dkLen, which you
|
||||
* can encode as you wish.
|
||||
*/
|
||||
pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen) {
|
||||
pbkdf2Generate : function pbkdf2Generate(P, S, c, dkLen,
|
||||
hmacAlg=Ci.nsICryptoHMAC.SHA1, hmacLen=20) {
|
||||
|
||||
// We don't have a default in the algo itself, as NSS does.
|
||||
// Use the constant.
|
||||
if (!dkLen) {
|
||||
dkLen = SYNC_KEY_DECODED_LENGTH;
|
||||
}
|
||||
|
||||
/* For HMAC-SHA-1 */
|
||||
const HLEN = 20;
|
||||
|
||||
function F(S, c, i, h) {
|
||||
|
||||
function XOR(a, b, isA) {
|
||||
@ -216,27 +220,27 @@ this.CryptoUtils = {
|
||||
}
|
||||
|
||||
ret = U[0];
|
||||
for (j = 1; j < c; j++) {
|
||||
for (let j = 1; j < c; j++) {
|
||||
ret = CommonUtils.byteArrayToString(XOR(ret, U[j]));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
let l = Math.ceil(dkLen / HLEN);
|
||||
let r = dkLen - ((l - 1) * HLEN);
|
||||
let l = Math.ceil(dkLen / hmacLen);
|
||||
let r = dkLen - ((l - 1) * hmacLen);
|
||||
|
||||
// Reuse the key and the hasher. Remaking them 4096 times is 'spensive.
|
||||
let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA1,
|
||||
let h = CryptoUtils.makeHMACHasher(hmacAlg,
|
||||
CryptoUtils.makeHMACKey(P));
|
||||
|
||||
T = [];
|
||||
let T = [];
|
||||
for (let i = 0; i < l;) {
|
||||
T[i] = F(S, c, ++i, h);
|
||||
}
|
||||
|
||||
let ret = "";
|
||||
for (i = 0; i < l-1;) {
|
||||
for (let i = 0; i < l-1;) {
|
||||
ret += T[i++];
|
||||
}
|
||||
ret += T[l - 1].substr(0, r);
|
||||
|
@ -1,15 +1,166 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Evil.
|
||||
let btoa = Cu.import("resource://services-common/utils.js").btoa;
|
||||
// XXX until bug 937114 is fixed
|
||||
Cu.importGlobalProperties(['btoa']);
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
let {bytesAsHex: b2h} = CommonUtils;
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function test_pbkdf2() {
|
||||
let symmKey16 = CryptoUtils.pbkdf2Generate("secret phrase", "DNXPzPpiwn", 4096, 16);
|
||||
do_check_eq(symmKey16.length, 16);
|
||||
do_check_eq(btoa(symmKey16), "d2zG0d2cBfXnRwMUGyMwyg==");
|
||||
do_check_eq(CommonUtils.encodeBase32(symmKey16), "O5WMNUO5TQC7LZ2HAMKBWIZQZI======");
|
||||
let symmKey32 = CryptoUtils.pbkdf2Generate("passphrase", "salt", 4096, 32);
|
||||
do_check_eq(symmKey32.length, 32);
|
||||
});
|
||||
|
||||
// http://tools.ietf.org/html/rfc6070
|
||||
// PBKDF2 HMAC-SHA1 Test Vectors
|
||||
add_task(function test_pbkdf2_hmac_sha1() {
|
||||
let pbkdf2 = CryptoUtils.pbkdf2Generate;
|
||||
let vectors = [
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 1,
|
||||
dkLen: 20,
|
||||
DK: h("0c 60 c8 0f 96 1f 0e 71"+
|
||||
"f3 a9 b5 24 af 60 12 06"+
|
||||
"2f e0 37 a6"), // (20 octets)
|
||||
},
|
||||
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 2,
|
||||
dkLen: 20,
|
||||
DK: h("ea 6c 01 4d c7 2d 6f 8c"+
|
||||
"cd 1e d9 2a ce 1d 41 f0"+
|
||||
"d8 de 89 57"), // (20 octets)
|
||||
},
|
||||
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 4096,
|
||||
dkLen: 20,
|
||||
DK: h("4b 00 79 01 b7 65 48 9a"+
|
||||
"be ad 49 d9 26 f7 21 d0"+
|
||||
"65 a4 29 c1"), // (20 octets)
|
||||
},
|
||||
|
||||
// XXX Uncomment the following test after Bug 968567 lands
|
||||
//
|
||||
// XXX As it stands, I estimate that the CryptoUtils implementation will
|
||||
// take approximately 16 hours in my 2.3GHz MacBook to perform this many
|
||||
// rounds.
|
||||
//
|
||||
// {P: "password", // (8 octets)
|
||||
// S: "salt" // (4 octets)
|
||||
// c: 16777216,
|
||||
// dkLen = 20,
|
||||
// DK: h("ee fe 3d 61 cd 4d a4 e4"+
|
||||
// "e9 94 5b 3d 6b a2 15 8c"+
|
||||
// "26 34 e9 84"), // (20 octets)
|
||||
// },
|
||||
|
||||
{P: "passwordPASSWORDpassword", // (24 octets)
|
||||
S: "saltSALTsaltSALTsaltSALTsaltSALTsalt", // (36 octets)
|
||||
c: 4096,
|
||||
dkLen: 25,
|
||||
DK: h("3d 2e ec 4f e4 1c 84 9b"+
|
||||
"80 c8 d8 36 62 c0 e4 4a"+
|
||||
"8b 29 1a 96 4c f2 f0 70"+
|
||||
"38"), // (25 octets)
|
||||
|
||||
},
|
||||
|
||||
{P: "pass\0word", // (9 octets)
|
||||
S: "sa\0lt", // (5 octets)
|
||||
c: 4096,
|
||||
dkLen: 16,
|
||||
DK: h("56 fa 6a a7 55 48 09 9d"+
|
||||
"cc 37 d7 f0 34 25 e0 c3"), // (16 octets)
|
||||
},
|
||||
];
|
||||
|
||||
for (let v of vectors) {
|
||||
do_check_eq(v.DK, b2h(pbkdf2(v.P, v.S, v.c, v.dkLen)));
|
||||
}
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// I can't find any normative ietf test vectors for pbkdf2 hmac-sha256.
|
||||
// The following vectors are derived with the same inputs as above (the sha1
|
||||
// test). Results verified by users here:
|
||||
// https://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors
|
||||
add_task(function test_pbkdf2_hmac_sha256() {
|
||||
let pbkdf2 = CryptoUtils.pbkdf2Generate;
|
||||
let vectors = [
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 1,
|
||||
dkLen: 32,
|
||||
DK: h("12 0f b6 cf fc f8 b3 2c"+
|
||||
"43 e7 22 52 56 c4 f8 37"+
|
||||
"a8 65 48 c9 2c cc 35 48"+
|
||||
"08 05 98 7c b7 0b e1 7b"), // (32 octets)
|
||||
},
|
||||
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 2,
|
||||
dkLen: 32,
|
||||
DK: h("ae 4d 0c 95 af 6b 46 d3"+
|
||||
"2d 0a df f9 28 f0 6d d0"+
|
||||
"2a 30 3f 8e f3 c2 51 df"+
|
||||
"d6 e2 d8 5a 95 47 4c 43"), // (32 octets)
|
||||
},
|
||||
|
||||
{P: "password", // (8 octets)
|
||||
S: "salt", // (4 octets)
|
||||
c: 4096,
|
||||
dkLen: 32,
|
||||
DK: h("c5 e4 78 d5 92 88 c8 41"+
|
||||
"aa 53 0d b6 84 5c 4c 8d"+
|
||||
"96 28 93 a0 01 ce 4e 11"+
|
||||
"a4 96 38 73 aa 98 13 4a"), // (32 octets)
|
||||
},
|
||||
|
||||
{P: "passwordPASSWORDpassword", // (24 octets)
|
||||
S: "saltSALTsaltSALTsaltSALTsaltSALTsalt", // (36 octets)
|
||||
c: 4096,
|
||||
dkLen: 40,
|
||||
DK: h("34 8c 89 db cb d3 2b 2f"+
|
||||
"32 d8 14 b8 11 6e 84 cf"+
|
||||
"2b 17 34 7e bc 18 00 18"+
|
||||
"1c 4e 2a 1f b8 dd 53 e1"+
|
||||
"c6 35 51 8c 7d ac 47 e9"), // (40 octets)
|
||||
},
|
||||
|
||||
{P: "pass\0word", // (9 octets)
|
||||
S: "sa\0lt", // (5 octets)
|
||||
c: 4096,
|
||||
dkLen: 16,
|
||||
DK: h("89 b6 9d 05 16 f8 29 89"+
|
||||
"3c 69 62 26 65 0a 86 87"), // (16 octets)
|
||||
},
|
||||
];
|
||||
|
||||
for (let v of vectors) {
|
||||
do_check_eq(v.DK,
|
||||
b2h(pbkdf2(v.P, v.S, v.c, v.dkLen, Ci.nsICryptoHMAC.SHA256, 32)));
|
||||
}
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// turn formatted test vectors into normal hex strings
|
||||
function h(hexStr) {
|
||||
return hexStr.replace(/\s+/g, "");
|
||||
}
|
||||
|
139
services/fxaccounts/Credentials.jsm
Normal file
139
services/fxaccounts/Credentials.jsm
Normal file
@ -0,0 +1,139 @@
|
||||
/* 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 module implements client-side key stretching for use in Firefox
|
||||
* Accounts account creation and login.
|
||||
*
|
||||
* See https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Credentials"];
|
||||
|
||||
const {utils: Cu, interfaces: Ci} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/";
|
||||
const PBKDF2_ROUNDS = 1000;
|
||||
const STRETCHED_PW_LENGTH_BYTES = 32;
|
||||
const HKDF_SALT = CommonUtils.hexToBytes("00");
|
||||
const HKDF_LENGTH = 32;
|
||||
const HMAC_ALGORITHM = Ci.nsICryptoHMAC.SHA256;
|
||||
const HMAC_LENGTH = 32;
|
||||
|
||||
// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
|
||||
// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
|
||||
// default.
|
||||
const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
|
||||
try {
|
||||
this.LOG_LEVEL =
|
||||
Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
|
||||
&& Services.prefs.getCharPref(PREF_LOG_LEVEL);
|
||||
} catch (e) {
|
||||
this.LOG_LEVEL = Log.Level.Error;
|
||||
}
|
||||
|
||||
let log = Log.repository.getLogger("Identity.FxAccounts");
|
||||
log.level = LOG_LEVEL;
|
||||
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
|
||||
|
||||
this.Credentials = Object.freeze({
|
||||
/**
|
||||
* Make constants accessible to tests
|
||||
*/
|
||||
constants: {
|
||||
PROTOCOL_VERSION: PROTOCOL_VERSION,
|
||||
PBKDF2_ROUNDS: PBKDF2_ROUNDS,
|
||||
STRETCHED_PW_LENGTH_BYTES: STRETCHED_PW_LENGTH_BYTES,
|
||||
HKDF_SALT: HKDF_SALT,
|
||||
HKDF_LENGTH: HKDF_LENGTH,
|
||||
HMAC_ALGORITHM: HMAC_ALGORITHM,
|
||||
HMAC_LENGTH: HMAC_LENGTH,
|
||||
},
|
||||
|
||||
/**
|
||||
* KW function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
|
||||
*
|
||||
* keyWord derivation for use as a salt.
|
||||
*
|
||||
*
|
||||
* @param {String} context String for use in generating salt
|
||||
*
|
||||
* @return {bitArray} the salt
|
||||
*
|
||||
* Note that PROTOCOL_VERSION does not refer in any way to the version of the
|
||||
* Firefox Accounts API.
|
||||
*/
|
||||
keyWord: function(context) {
|
||||
return CommonUtils.stringToBytes(PROTOCOL_VERSION + context);
|
||||
},
|
||||
|
||||
/**
|
||||
* KWE function from https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
|
||||
*
|
||||
* keyWord extended with a name and an email.
|
||||
*
|
||||
* @param {String} name The name of the salt
|
||||
* @param {String} email The email of the user.
|
||||
*
|
||||
* @return {bitArray} the salt combination with the namespace
|
||||
*
|
||||
* Note that PROTOCOL_VERSION does not refer in any way to the version of the
|
||||
* Firefox Accounts API.
|
||||
*/
|
||||
keyWordExtended: function(name, email) {
|
||||
return CommonUtils.stringToBytes(PROTOCOL_VERSION + name + ':' + email);
|
||||
},
|
||||
|
||||
setup: function(emailInput, passwordInput, options={}) {
|
||||
let deferred = Promise.defer();
|
||||
log.debug("setup credentials for " + emailInput);
|
||||
|
||||
let hkdfSalt = options.hkdfSalt || HKDF_SALT;
|
||||
let hkdfLength = options.hkdfLength || HKDF_LENGTH;
|
||||
let hmacLength = options.hmacLength || HMAC_LENGTH;
|
||||
let hmacAlgorithm = options.hmacAlgorithm || HMAC_ALGORITHM;
|
||||
let stretchedPWLength = options.stretchedPassLength || STRETCHED_PW_LENGTH_BYTES;
|
||||
let pbkdf2Rounds = options.pbkdf2Rounds || PBKDF2_ROUNDS;
|
||||
|
||||
let result = {
|
||||
emailUTF8: emailInput,
|
||||
passwordUTF8: passwordInput,
|
||||
};
|
||||
|
||||
let password = CommonUtils.encodeUTF8(passwordInput);
|
||||
let salt = this.keyWordExtended("quickStretch", emailInput);
|
||||
|
||||
let runnable = () => {
|
||||
let start = Date.now();
|
||||
let quickStretchedPW = CryptoUtils.pbkdf2Generate(
|
||||
password, salt, pbkdf2Rounds, stretchedPWLength, hmacAlgorithm, hmacLength);
|
||||
|
||||
result.quickStretchedPW = quickStretchedPW;
|
||||
|
||||
result.authPW =
|
||||
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("authPW"), hkdfLength);
|
||||
|
||||
result.unwrapBKey =
|
||||
CryptoUtils.hkdf(quickStretchedPW, hkdfSalt, this.keyWord("unwrapBkey"), hkdfLength);
|
||||
|
||||
log.debug("Credentials set up after " + (Date.now() - start) + " ms");
|
||||
deferred.resolve(result);
|
||||
}
|
||||
|
||||
Services.tm.currentThread.dispatch(runnable,
|
||||
Ci.nsIThread.DISPATCH_NORMAL);
|
||||
log.debug("Dispatched thread for credentials setup crypto work");
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/hawk.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://gre/modules/Credentials.jsm");
|
||||
|
||||
// Default can be changed by the preference 'identity.fxaccounts.auth.uri'
|
||||
let _host = "https://api-accounts.dev.lcip.org/v1";
|
||||
@ -21,34 +22,6 @@ try {
|
||||
} catch(keepDefault) {}
|
||||
|
||||
const HOST = _host;
|
||||
const PROTOCOL_VERSION = "identity.mozilla.com/picl/v1/";
|
||||
|
||||
function KW(context) {
|
||||
// This is used as a salt. It's specified by the protocol. Note that the
|
||||
// value of PROTOCOL_VERSION does not refer in any wy to the version of the
|
||||
// Firefox Accounts API. For this reason, it is not exposed as a pref.
|
||||
//
|
||||
// See:
|
||||
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#creating-the-account
|
||||
return PROTOCOL_VERSION + context;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -92,23 +65,18 @@ this.FxAccountsClient.prototype = {
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to an object:
|
||||
* {
|
||||
* uid: the user's unique ID
|
||||
* sessionToken: a session token
|
||||
* uid: the user's unique ID (hex)
|
||||
* sessionToken: a session token (hex)
|
||||
* keyFetchToken: a key fetch token (hex)
|
||||
* }
|
||||
*/
|
||||
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;
|
||||
});
|
||||
signUp: function(email, password) {
|
||||
return Credentials.setup(email, password).then((creds) => {
|
||||
let data = {
|
||||
email: creds.emailUTF8,
|
||||
authPW: CommonUtils.bytesAsHex(creds.authPW),
|
||||
};
|
||||
return this._request("/account/create", "POST", null, data);
|
||||
});
|
||||
},
|
||||
|
||||
@ -122,15 +90,20 @@ this.FxAccountsClient.prototype = {
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to an object:
|
||||
* {
|
||||
* uid: the user's unique ID
|
||||
* sessionToken: a session token
|
||||
* uid: the user's unique ID (hex)
|
||||
* sessionToken: a session token (hex)
|
||||
* keyFetchToken: a key fetch token (hex)
|
||||
* verified: 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});
|
||||
return Credentials.setup(email, password).then((creds) => {
|
||||
let data = {
|
||||
email: creds.emailUTF8,
|
||||
authPW: CommonUtils.bytesAsHex(creds.authPW),
|
||||
};
|
||||
return this._request("/account/login", "POST", null, data);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -177,15 +150,16 @@ this.FxAccountsClient.prototype = {
|
||||
* @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
|
||||
* kA: an encryption key for recevorable data (bytes)
|
||||
* wrapKB: an encryption key that requires knowledge of the
|
||||
* user's password (bytes)
|
||||
* }
|
||||
*/
|
||||
accountKeys: function (keyFetchTokenHex) {
|
||||
let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
|
||||
let keyRequestKey = creds.extra.slice(0, 32);
|
||||
let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
|
||||
KW("account/keys"), 3 * 32);
|
||||
Credentials.keyWord("account/keys"), 3 * 32);
|
||||
let respHMACKey = morecreds.slice(0, 32);
|
||||
let respXORKey = morecreds.slice(32, 96);
|
||||
|
||||
@ -251,22 +225,25 @@ this.FxAccountsClient.prototype = {
|
||||
* 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) => {
|
||||
log.error("accountExists: error: " + JSON.stringify(err));
|
||||
// the account doesn't exist
|
||||
if (err.errno === 102) {
|
||||
log.debug("returning false for errno 102");
|
||||
return this.signIn(email, "").then(
|
||||
(cantHappen) => {
|
||||
throw new Error("How did I sign in with an empty password?");
|
||||
},
|
||||
(expectedError) => {
|
||||
switch (expectedError.errno) {
|
||||
case ERRNO_ACCOUNT_DOES_NOT_EXIST:
|
||||
return false;
|
||||
}
|
||||
// propogate other request errors
|
||||
throw err;
|
||||
break;
|
||||
case ERRNO_INCORRECT_PASSWORD:
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
// not so expected, any more ...
|
||||
throw expectedError;
|
||||
break;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -291,7 +268,7 @@ this.FxAccountsClient.prototype = {
|
||||
*/
|
||||
_deriveHawkCredentials: function (tokenHex, context, size) {
|
||||
let token = CommonUtils.hexToBytes(tokenHex);
|
||||
let out = CryptoUtils.hkdf(token, undefined, KW(context), size || 3 * 32);
|
||||
let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size || 3 * 32);
|
||||
|
||||
return {
|
||||
algorithm: "sha256",
|
||||
@ -333,11 +310,13 @@ this.FxAccountsClient.prototype = {
|
||||
let response = JSON.parse(responseText);
|
||||
deferred.resolve(response);
|
||||
} catch (err) {
|
||||
log.error("json parse error on response: " + responseText);
|
||||
deferred.reject({error: err});
|
||||
}
|
||||
},
|
||||
|
||||
(error) => {
|
||||
log.error("request error: " + JSON.stringify(error));
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
|
@ -49,7 +49,7 @@ this.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout";
|
||||
// Server errno.
|
||||
// From https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format
|
||||
this.ERRNO_ACCOUNT_ALREADY_EXISTS = 101;
|
||||
this.ERRNO_ACCOUNT_DOES_NOT_EXISTS = 102;
|
||||
this.ERRNO_ACCOUNT_DOES_NOT_EXIST = 102;
|
||||
this.ERRNO_INCORRECT_PASSWORD = 103;
|
||||
this.ERRNO_UNVERIFIED_ACCOUNT = 104;
|
||||
this.ERRNO_INVALID_VERIFICATION_CODE = 105;
|
||||
@ -68,7 +68,7 @@ this.ERRNO_UNKNOWN_ERROR = 999;
|
||||
|
||||
// Errors.
|
||||
this.ERROR_ACCOUNT_ALREADY_EXISTS = "ACCOUNT_ALREADY_EXISTS";
|
||||
this.ERROR_ACCOUNT_DOES_NOT_EXISTS = "ACCOUNT_DOES_NOT_EXISTS";
|
||||
this.ERROR_ACCOUNT_DOES_NOT_EXIST = "ACCOUNT_DOES_NOT_EXIST";
|
||||
this.ERROR_ALREADY_SIGNED_IN_USER = "ALREADY_SIGNED_IN_USER";
|
||||
this.ERROR_INVALID_ACCOUNTID = "INVALID_ACCOUNTID";
|
||||
this.ERROR_INVALID_AUDIENCE = "INVALID_AUDIENCE";
|
||||
@ -96,7 +96,7 @@ this.ERROR_UNVERIFIED_ACCOUNT = "UNVERIFIED_ACCOUNT";
|
||||
// Error matching.
|
||||
this.SERVER_ERRNO_TO_ERROR = {};
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_ALREADY_EXISTS] = ERROR_ACCOUNT_ALREADY_EXISTS;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_DOES_NOT_EXISTS] = ERROR_ACCOUNT_DOES_NOT_EXISTS;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_ACCOUNT_DOES_NOT_EXIST] = ERROR_ACCOUNT_DOES_NOT_EXIST;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INCORRECT_PASSWORD] = ERROR_INVALID_PASSWORD;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_UNVERIFIED_ACCOUNT] = ERROR_UNVERIFIED_ACCOUNT;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_VERIFICATION_CODE] = ERROR_INVALID_VERIFICATION_CODE;
|
||||
|
@ -9,6 +9,7 @@ PARALLEL_DIRS += ['interfaces']
|
||||
TEST_DIRS += ['tests']
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'Credentials.jsm',
|
||||
'FxAccounts.jsm',
|
||||
'FxAccountsClient.jsm',
|
||||
'FxAccountsCommon.js',
|
||||
|
@ -4,6 +4,7 @@
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
||||
const FAKE_SESSION_TOKEN = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
|
||||
|
||||
@ -101,131 +102,388 @@ add_task(function test_500_error() {
|
||||
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});
|
||||
add_task(function test_signUp() {
|
||||
let creationMessage = JSON.stringify({
|
||||
uid: "uid",
|
||||
sessionToken: "sessionToken",
|
||||
keyFetchToken: "keyFetchToken"
|
||||
});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 101, error: "account exists"});
|
||||
let created = false;
|
||||
|
||||
let authStarts = 0;
|
||||
let server = httpd_setup({
|
||||
"/account/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
|
||||
function writeResp(response, msg) {
|
||||
response.bodyOutputStream.write(msg, msg.length);
|
||||
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors
|
||||
do_check_eq(jsonBody.email, "andré@example.org");
|
||||
|
||||
if (!created) {
|
||||
do_check_eq(jsonBody.authPW, "247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375");
|
||||
created = true;
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
return response.bodyOutputStream.write(creationMessage, creationMessage.length);
|
||||
}
|
||||
|
||||
// Error trying to create same account a second time
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.signUp('andré@example.org', 'pässwörd');
|
||||
do_check_eq("uid", result.uid);
|
||||
do_check_eq("sessionToken", result.sessionToken);
|
||||
do_check_eq("keyFetchToken", result.keyFetchToken);
|
||||
|
||||
// Try to create account again. Triggers error path.
|
||||
try {
|
||||
result = yield client.signUp('andré@example.org', 'pässwörd');
|
||||
} catch(expectedError) {
|
||||
do_check_eq(101, expectedError.errno);
|
||||
}
|
||||
|
||||
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");
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
add_task(function test_signIn() {
|
||||
let sessionMessage = JSON.stringify({sessionToken: FAKE_SESSION_TOKEN});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let server = httpd_setup({
|
||||
"/account/login": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
|
||||
if (jsonBody.email == "mé@example.com") {
|
||||
do_check_eq(jsonBody.authPW, "08b9d111196b8408e8ed92439da49206c8ecfbf343df0ae1ecefcd1e0174a8b6");
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
|
||||
},
|
||||
"/recovery_email/status": function(request, response) {
|
||||
return response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
|
||||
}
|
||||
|
||||
// Error trying to sign in to nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.signIn('mé@example.com', 'bigsecret');
|
||||
do_check_eq(FAKE_SESSION_TOKEN, result.sessionToken);
|
||||
|
||||
// Trigger error path
|
||||
try {
|
||||
result = yield client.signIn("yøü@bad.example.org", "nofear");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_signOut() {
|
||||
let signoutMessage = JSON.stringify({});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let signedOut = false;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/session/destroy": function(request, response) {
|
||||
if (!signedOut) {
|
||||
signedOut = true;
|
||||
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"));
|
||||
return response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
|
||||
}
|
||||
|
||||
// Error trying to sign out of nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.signOut("FakeSession");
|
||||
do_check_eq(typeof result, "object");
|
||||
|
||||
// Trigger error path
|
||||
try {
|
||||
result = yield client.signOut("FakeSession");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_recoveryEmailStatus() {
|
||||
let emailStatus = JSON.stringify({verified: true});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let tries = 0;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/recovery_email/status": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
if (tries === 0) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
|
||||
},
|
||||
"/certificate/sign": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
return response.bodyOutputStream.write(emailStatus, emailStatus.length);
|
||||
}
|
||||
|
||||
// Second call gets an error trying to query a nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.recoveryEmailStatus(FAKE_SESSION_TOKEN);
|
||||
do_check_eq(result.verified, true);
|
||||
|
||||
// Trigger error path
|
||||
try {
|
||||
result = yield client.recoveryEmailStatus("some bogus session");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_resendVerificationEmail() {
|
||||
let emptyMessage = "{}";
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let tries = 0;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/recovery_email/resend_code": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
if (tries === 0) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
return response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
|
||||
}
|
||||
|
||||
// Second call gets an error trying to query a nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
return response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result = yield client.resendVerificationEmail(FAKE_SESSION_TOKEN);
|
||||
do_check_eq(JSON.stringify(result), emptyMessage);
|
||||
|
||||
// Trigger error path
|
||||
try {
|
||||
result = yield client.resendVerificationEmail("some bogus session");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_accountKeys() {
|
||||
// Vectors: https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#.2Faccount.2Fkeys
|
||||
|
||||
let keyFetch = h("8081828384858687 88898a8b8c8d8e8f"+
|
||||
"9091929394959697 98999a9b9c9d9e9f");
|
||||
|
||||
let response = h("ee5c58845c7c9412 b11bbd20920c2fdd"+
|
||||
"d83c33c9cd2c2de2 d66b222613364636"+
|
||||
"c2c0f8cfbb7c6304 72c0bd88451342c6"+
|
||||
"c05b14ce342c5ad4 6ad89e84464c993c"+
|
||||
"3927d30230157d08 17a077eef4b20d97"+
|
||||
"6f7a97363faf3f06 4c003ada7d01aa70");
|
||||
|
||||
let kA = h("2021222324252627 28292a2b2c2d2e2f"+
|
||||
"3031323334353637 38393a3b3c3d3e3f");
|
||||
|
||||
let wrapKB = h("4041424344454647 48494a4b4c4d4e4f"+
|
||||
"5051525354555657 58595a5b5c5d5e5f");
|
||||
|
||||
let responseMessage = JSON.stringify({bundle: response});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let emptyMessage = "{}";
|
||||
let attempt = 0;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/account/keys": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
attempt += 1;
|
||||
|
||||
switch(attempt) {
|
||||
case 1:
|
||||
// First time succeeds
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(responseMessage, responseMessage.length);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Second time, return no bundle to trigger client error
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Return gibberish to trigger client MAC error
|
||||
let garbage = response;
|
||||
garbage[0] = 0; // tweak a byte
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(responseMessage, responseMessage.length);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
// Trigger error for nonexistent account
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
|
||||
// First try, all should be good
|
||||
let result = yield client.accountKeys(keyFetch);
|
||||
do_check_eq(CommonUtils.hexToBytes(kA), result.kA);
|
||||
do_check_eq(CommonUtils.hexToBytes(wrapKB), result.wrapKB);
|
||||
|
||||
// Second try, empty bundle should trigger error
|
||||
try {
|
||||
result = yield client.accountKeys(keyFetch);
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.message, "failed to retrieve keys");
|
||||
}
|
||||
|
||||
// Third try, bad bundle results in MAC error
|
||||
try {
|
||||
result = yield client.accountKeys(keyFetch);
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.message, "error unbundling encryption keys");
|
||||
}
|
||||
|
||||
// Fourth try, pretend account doesn't exist
|
||||
try {
|
||||
result = yield client.accountKeys(keyFetch);
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_signCertificate() {
|
||||
let certSignMessage = JSON.stringify({cert: {bar: "baz"}});
|
||||
let errorMessage = JSON.stringify({code: 400, errno: 102, error: "doesn't exist"});
|
||||
let tries = 0;
|
||||
|
||||
let server = httpd_setup({
|
||||
"/certificate/sign": function(request, response) {
|
||||
do_check_true(request.hasHeader("Authorization"));
|
||||
|
||||
if (tries === 0) {
|
||||
tries += 1;
|
||||
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++;
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
|
||||
}
|
||||
|
||||
// Second attempt, trigger error
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
});
|
||||
|
||||
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(FAKE_SESSION_TOKEN);
|
||||
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);
|
||||
let result = yield client.signCertificate(FAKE_SESSION_TOKEN, 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);
|
||||
// Account doesn't exist
|
||||
try {
|
||||
result = yield client.accountExists('hey3@example.com');
|
||||
} catch(e) {
|
||||
do_check_eq(e.errno, 107);
|
||||
result = yield client.signCertificate("bogus", JSON.stringify({foo: "bar"}), 600);
|
||||
} catch(expectedError) {
|
||||
do_check_eq(102, expectedError.errno);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
add_task(function test_error_response() {
|
||||
let errorMessage = JSON.stringify({error: "Oops", code: 400, errno: 99});
|
||||
add_task(function test_accountExists() {
|
||||
let sessionMessage = JSON.stringify({sessionToken: FAKE_SESSION_TOKEN});
|
||||
let existsMessage = JSON.stringify({error: "wrong password", code: 400, errno: 103});
|
||||
let doesntExistMessage = JSON.stringify({error: "no such account", code: 400, errno: 102});
|
||||
let emptyMessage = "{}";
|
||||
|
||||
let server = httpd_setup(
|
||||
{
|
||||
"/raw_password/session/create": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let server = httpd_setup({
|
||||
"/account/login": function(request, response) {
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let jsonBody = JSON.parse(body);
|
||||
|
||||
response.setStatusLine(request.httpVersion, 400, "NOT OK");
|
||||
response.bodyOutputStream.write(errorMessage, errorMessage.length);
|
||||
},
|
||||
}
|
||||
);
|
||||
switch (jsonBody.email) {
|
||||
// We'll test that these users' accounts exist
|
||||
case "i.exist@example.com":
|
||||
case "i.also.exist@example.com":
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
response.bodyOutputStream.write(existsMessage, existsMessage.length);
|
||||
break;
|
||||
|
||||
// This user's account doesn't exist
|
||||
case "i.dont.exist@example.com":
|
||||
response.setStatusLine(request.httpVersion, 400, "Bad request");
|
||||
response.bodyOutputStream.write(doesntExistMessage, doesntExistMessage.length);
|
||||
break;
|
||||
|
||||
// This user throws an unexpected response
|
||||
// This will reject the client signIn promise
|
||||
case "i.break.things@example.com":
|
||||
response.setStatusLine(request.httpVersion, 500, "Alas");
|
||||
response.bodyOutputStream.write(emptyMessage, emptyMessage.length);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unexpected login from " + jsonBody.email);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
let client = new FxAccountsClient(server.baseURI);
|
||||
let result;
|
||||
|
||||
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);
|
||||
result = yield client.accountExists("i.exist@example.com");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.code, 400);
|
||||
do_check_eq(expectedError.errno, 103);
|
||||
}
|
||||
|
||||
try {
|
||||
result = yield client.accountExists("i.also.exist@example.com");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.errno, 103);
|
||||
}
|
||||
|
||||
try {
|
||||
result = yield client.accountExists("i.dont.exist@example.com");
|
||||
} catch(expectedError) {
|
||||
do_check_eq(expectedError.errno, 102);
|
||||
}
|
||||
|
||||
try {
|
||||
result = yield client.accountExists("i.break.things@example.com");
|
||||
} catch(unexpectedError) {
|
||||
do_check_eq(unexpectedError.code, 500);
|
||||
}
|
||||
|
||||
yield deferredStop(server);
|
||||
});
|
||||
|
||||
// turn formatted test vectors into normal hex strings
|
||||
function h(hexStr) {
|
||||
return hexStr.replace(/\s+/g, "");
|
||||
}
|
||||
|
120
services/fxaccounts/tests/xpcshell/test_credentials.js
Normal file
120
services/fxaccounts/tests/xpcshell/test_credentials.js
Normal file
@ -0,0 +1,120 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/Credentials.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
||||
let {hexToBytes: h2b,
|
||||
hexAsString: h2s,
|
||||
stringAsHex: s2h,
|
||||
bytesAsHex: b2h} = CommonUtils;
|
||||
|
||||
// Test vectors for the "onepw" protocol:
|
||||
// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-test-vectors
|
||||
let vectors = {
|
||||
"client stretch-KDF": {
|
||||
email:
|
||||
h("616e6472c3a94065 78616d706c652e6f 7267"),
|
||||
password:
|
||||
h("70c3a4737377c3b6 7264"),
|
||||
quickStretchedPW:
|
||||
h("e4e8889bd8bd61ad 6de6b95c059d56e7 b50dacdaf62bd846 44af7e2add84345d"),
|
||||
authPW:
|
||||
h("247b675ffb4c4631 0bc87e26d712153a be5e1c90ef00a478 4594f97ef54f2375"),
|
||||
authSalt:
|
||||
h("00f0000000000000 0000000000000000 0000000000000000 0000000000000000"),
|
||||
},
|
||||
};
|
||||
|
||||
// A simple test suite with no utf8 encoding madness.
|
||||
add_task(function test_onepw_setup_credentials() {
|
||||
let email = "francine@example.org";
|
||||
let password = CommonUtils.encodeUTF8("i like pie");
|
||||
|
||||
let pbkdf2 = CryptoUtils.pbkdf2Generate;
|
||||
let hkdf = CryptoUtils.hkdf;
|
||||
|
||||
// quickStretch the email
|
||||
let saltyEmail = Credentials.keyWordExtended("quickStretch", email);
|
||||
|
||||
do_check_eq(b2h(saltyEmail), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a6672616e63696e65406578616d706c652e6f7267");
|
||||
|
||||
let pbkdf2Rounds = 1000;
|
||||
let pbkdf2Len = 32;
|
||||
|
||||
let quickStretchedPW = pbkdf2(password, saltyEmail, pbkdf2Rounds, pbkdf2Len, Ci.nsICryptoHMAC.SHA256, 32);
|
||||
let quickStretchedActual = "6b88094c1c73bbf133223f300d101ed70837af48d9d2c1b6e7d38804b20cdde4";
|
||||
do_check_eq(b2h(quickStretchedPW), quickStretchedActual);
|
||||
|
||||
// obtain hkdf info
|
||||
let authKeyInfo = Credentials.keyWord('authPW');
|
||||
do_check_eq(b2h(authKeyInfo), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f617574685057");
|
||||
|
||||
// derive auth password
|
||||
let hkdfSalt = h2b("00");
|
||||
let hkdfLen = 32;
|
||||
let authPW = hkdf(quickStretchedPW, hkdfSalt, authKeyInfo, hkdfLen);
|
||||
|
||||
do_check_eq(b2h(authPW), "4b8dec7f48e7852658163601ff766124c312f9392af6c3d4e1a247eb439be342");
|
||||
|
||||
// derive unwrap key
|
||||
let unwrapKeyInfo = Credentials.keyWord('unwrapBkey');
|
||||
let unwrapKey = hkdf(quickStretchedPW, hkdfSalt, unwrapKeyInfo, hkdfLen);
|
||||
|
||||
do_check_eq(b2h(unwrapKey), "8ff58975be391338e4ec5d7138b5ed7b65c7d1bfd1f3a4f93e05aa47d5b72be9");
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_client_stretch_kdf() {
|
||||
let pbkdf2 = CryptoUtils.pbkdf2Generate;
|
||||
let hkdf = CryptoUtils.hkdf;
|
||||
let expected = vectors["client stretch-KDF"];
|
||||
|
||||
let emailUTF8 = h2s(expected.email);
|
||||
let passwordUTF8 = h2s(expected.password);
|
||||
|
||||
// Intermediate value from sjcl implementation in fxa-js-client
|
||||
// The key thing is the c3a9 sequence in "andré"
|
||||
let salt = Credentials.keyWordExtended("quickStretch", emailUTF8);
|
||||
do_check_eq(b2h(salt), "6964656e746974792e6d6f7a696c6c612e636f6d2f7069636c2f76312f717569636b537472657463683a616e6472c3a9406578616d706c652e6f7267");
|
||||
|
||||
let options = {
|
||||
stretchedPassLength: 32,
|
||||
pbkdf2Rounds: 1000,
|
||||
hmacAlgorithm: Ci.nsICryptoHMAC.SHA256,
|
||||
hmacLength: 32,
|
||||
hkdfSalt: h2b("00"),
|
||||
hkdfLength: 32,
|
||||
};
|
||||
|
||||
let results = yield Credentials.setup(emailUTF8, passwordUTF8, options);
|
||||
|
||||
do_check_eq(emailUTF8, results.emailUTF8,
|
||||
"emailUTF8 is wrong");
|
||||
|
||||
do_check_eq(passwordUTF8, results.passwordUTF8,
|
||||
"passwordUTF8 is wrong");
|
||||
|
||||
do_check_eq(expected.quickStretchedPW, b2h(results.quickStretchedPW),
|
||||
"quickStretchedPW is wrong");
|
||||
|
||||
do_check_eq(expected.authPW, b2h(results.authPW),
|
||||
"authPW is wrong");
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
// End of tests
|
||||
// Utility functions follow
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// turn formatted test vectors into normal hex strings
|
||||
function h(hexStr) {
|
||||
return hexStr.replace(/\s+/g, "");
|
||||
}
|
@ -4,6 +4,7 @@ tail =
|
||||
|
||||
[test_accounts.js]
|
||||
[test_client.js]
|
||||
[test_credentials.js]
|
||||
[test_manager.js]
|
||||
run-if = appname == 'b2g'
|
||||
reason = FxAccountsManager is only available for B2G for now
|
||||
|
Loading…
Reference in New Issue
Block a user