merge m-c to fx-team

This commit is contained in:
Tim Taubert 2014-02-08 21:34:18 +01:00
commit d782e85d01
43 changed files with 1376 additions and 238 deletions

View File

@ -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:

View File

@ -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);

View File

@ -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()

View File

@ -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);

View File

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

View File

@ -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",

View File

@ -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.

View File

@ -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);

View File

@ -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) {

View File

@ -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',

View File

@ -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]

View File

@ -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() {

View File

@ -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);

View File

@ -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();
});

View File

@ -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.");

View File

@ -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();
});

View File

@ -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));

View File

@ -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) {

View File

@ -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();
});
}
}

View File

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

View File

@ -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.
*

View File

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

View File

@ -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();
},

View File

@ -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);

View File

@ -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();

View File

@ -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, {

View File

@ -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 {

View File

@ -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,

View File

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

View File

@ -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 */

View File

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

View File

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

View File

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

View File

@ -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
*/

View File

@ -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);

View File

@ -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, "");
}

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ PARALLEL_DIRS += ['interfaces']
TEST_DIRS += ['tests']
EXTRA_JS_MODULES += [
'Credentials.jsm',
'FxAccounts.jsm',
'FxAccountsClient.jsm',
'FxAccountsCommon.js',

View File

@ -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, "");
}

View 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, "");
}

View File

@ -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