diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 94f66ba93ed..f69a01bb79d 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1176,7 +1176,7 @@ pref("devtools.profiler.ui.show-platform-data", false); pref("devtools.netmonitor.enabled", true); // The default Network Monitor UI settings -pref("devtools.netmonitor.panes-network-details-width", 450); +pref("devtools.netmonitor.panes-network-details-width", 550); pref("devtools.netmonitor.panes-network-details-height", 450); pref("devtools.netmonitor.statistics", true); pref("devtools.netmonitor.filters", "[\"all\"]"); diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index 3850d1ac7d7..c251625b955 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -14,6 +14,10 @@ %endif } +#main-window[customize-entered] { + min-width: -moz-fit-content; +} + searchbar { -moz-binding: url("chrome://browser/content/search/search.xml#searchbar"); } diff --git a/browser/base/content/newtab/newTab.js b/browser/base/content/newtab/newTab.js index af2f6a52ffb..a56a391bb01 100644 --- a/browser/base/content/newtab/newTab.js +++ b/browser/base/content/newtab/newTab.js @@ -13,7 +13,7 @@ Cu.import("resource://gre/modules/PageThumbs.jsm"); Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm"); Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm"); Cu.import("resource://gre/modules/NewTabUtils.jsm"); -Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +Cu.import("resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm"); diff --git a/browser/base/content/newtab/transformations.js b/browser/base/content/newtab/transformations.js index 7976b12f962..6ff123bd401 100644 --- a/browser/base/content/newtab/transformations.js +++ b/browser/base/content/newtab/transformations.js @@ -181,7 +181,7 @@ let gTransformation = { let deferred = Promise.defer(); batch.push(deferred.promise); - let cb = function () deferred.resolve(); + let cb = deferred.resolve; if (!cells[aIndex]) // The site disappeared from the grid, hide it. @@ -194,8 +194,9 @@ let gTransformation = { this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: cb}); }, this); - let wait = Promise.promised(function () callback && callback()); - wait.apply(null, batch); + if (callback) { + Promise.all(batch).then(callback); + } }, /** diff --git a/browser/base/content/newtab/updater.js b/browser/base/content/newtab/updater.js index 7b483e037f2..66924323f36 100644 --- a/browser/base/content/newtab/updater.js +++ b/browser/base/content/newtab/updater.js @@ -147,8 +147,7 @@ let gUpdater = { }); }); - let wait = Promise.promised(aCallback); - wait.apply(null, batch); + Promise.all(batch).then(aCallback); }, /** @@ -180,7 +179,6 @@ let gUpdater = { gTransformation.showSite(site, function () deferred.resolve()); }); - let wait = Promise.promised(aCallback); - wait.apply(null, batch); + Promise.all(batch).then(aCallback); } }; diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 53913233764..7e8d24241b5 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1101,6 +1101,22 @@ this._tabAttrModified(oldTab); this._tabAttrModified(this.mCurrentTab); + if (oldBrowser != newBrowser && + oldBrowser.docShell && + oldBrowser.docShell.contentViewer.inPermitUnload) { + // Since the user is switching away from a tab that has + // a beforeunload prompt active, we remove the prompt. + // This prevents confusing user flows like the following: + // 1. User attempts to close Firefox + // 2. User switches tabs (ingoring a beforeunload prompt) + // 3. User returns to tab, presses "Leave page" + let promptBox = this.getTabModalPromptBox(oldBrowser); + let prompts = promptBox.listPrompts(); + // NB: This code assumes that the beforeunload prompt + // is the top-most prompt on the tab. + promptBox.removePrompt(prompts[prompts.length - 1]); + } + // Adjust focus oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused); do { @@ -1879,7 +1895,6 @@ - diff --git a/browser/components/preferences/sync.xul b/browser/components/preferences/sync.xul index 0c96b695960..ba0c8bcc430 100644 --- a/browser/components/preferences/sync.xul +++ b/browser/components/preferences/sync.xul @@ -276,10 +276,7 @@ - diff --git a/browser/components/tabview/test/browser_tabview_bug599626.js b/browser/components/tabview/test/browser_tabview_bug599626.js index b1008cbe85d..47ecda3b0c7 100644 --- a/browser/components/tabview/test/browser_tabview_bug599626.js +++ b/browser/components/tabview/test/browser_tabview_bug599626.js @@ -1,6 +1,8 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + const TEST_URL = 'data:text/html,'; @@ -20,7 +22,10 @@ function onTabViewShown() { } function testStayOnPage(contentWindow, groupItemOne, groupItemTwo) { - whenDialogOpened(function (dialog) { + // We created a new tab group with a second tab above, so let's + // pick that second tab here and wait for its onbeforeunload dialog. + let browser = gBrowser.browsers[1]; + waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) { executeSoon(function () { is(gBrowser.tabs.length, 2, "The total number of tab is 2 when staying on the page"); @@ -34,14 +39,17 @@ function testStayOnPage(contentWindow, groupItemOne, groupItemTwo) { }); // stay on page - dialog.cancelDialog(); + btnStay.click(); }); closeGroupItem(groupItemTwo); } function testLeavePage(contentWindow, groupItemOne, groupItemTwo) { - whenDialogOpened(function (dialog) { + // The second tab hasn't been closed yet because we chose to stay. Wait + // for the onbeforeunload dialog again and leave the page this time. + let browser = gBrowser.browsers[1]; + waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) { // clean up and finish the test groupItemTwo.addSubscriber("close", function onClose() { groupItemTwo.removeSubscriber("close", onClose); @@ -55,31 +63,8 @@ function testLeavePage(contentWindow, groupItemOne, groupItemTwo) { }); // Leave page - dialog.acceptDialog(); + btnLeave.click(); }); closeGroupItem(groupItemTwo); } - -// ---------- -function whenDialogOpened(callback) { - let listener = { - onCloseWindow: function () {}, - onWindowTitleChange: function () {}, - - onOpenWindow: function (xulWin) { - let domWin = xulWin.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - - whenWindowLoaded(domWin, function () { - let dialog = domWin.document.querySelector("dialog"); - if (dialog) { - Services.wm.removeListener(listener); - callback(dialog); - } - }); - } - }; - - Services.wm.addListener(listener); -} diff --git a/browser/components/tabview/test/browser_tabview_bug626455.js b/browser/components/tabview/test/browser_tabview_bug626455.js index e6ffaa255f6..beabf825925 100644 --- a/browser/components/tabview/test/browser_tabview_bug626455.js +++ b/browser/components/tabview/test/browser_tabview_bug626455.js @@ -7,10 +7,10 @@ * Raymond Lee */ +"use strict"; + const TEST_URL = 'data:text/html,'; + 'function(e){e.returnValue="?"}'; let contentWindow; let activeGroup; @@ -23,25 +23,27 @@ function test() { activeGroup = contentWindow.GroupItems.getActiveGroupItem(); gBrowser.browsers[0].loadURI("data:text/html,

test for bug 626455, tab1"); - gBrowser.addTab(TEST_URL); - afterAllTabsLoaded(testStayOnPage); + let tab = gBrowser.addTab(TEST_URL); + afterAllTabsLoaded(() => testStayOnPage(tab)); }); } -function testStayOnPage() { - whenDialogOpened(function (dialog) { +function testStayOnPage(blockingTab) { + let browser = blockingTab.linkedBrowser; + waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) { // stay on page - dialog.cancelDialog(); + btnStay.click(); executeSoon(function () { showTabView(function () { is(gBrowser.tabs.length, 1, "The total number of tab is 1 when staying on the page"); - let location = gBrowser.browsers[0].currentURI.spec; - isnot(location.indexOf("onbeforeunload"), -1, - "The open tab is the expected one"); + // The other initial tab has been closed when trying to close the tab + // group. The only tab left is the one with the onbeforeunload dialog. + let url = gBrowser.browsers[0].currentURI.spec; + ok(url.contains("onbeforeunload"), "The open tab is the expected one"); is(contentWindow.GroupItems.getActiveGroupItem(), activeGroup, "Active group is still the same"); @@ -50,7 +52,7 @@ function testStayOnPage() { "Only one group is open"); // start the next test - testLeavePage(); + testLeavePage(gBrowser.tabs[0]); }); }); }); @@ -58,12 +60,11 @@ function testStayOnPage() { closeGroupItem(activeGroup); } -function testLeavePage() { - let dialogsAccepted = 0; - - whenDialogOpened(function onDialogOpened(dialog) { +function testLeavePage(blockingTab) { + let browser = blockingTab.linkedBrowser; + waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) { // Leave page - dialog.acceptDialog(); + btnLeave.click(); }); whenGroupClosed(activeGroup, finishTest); @@ -85,6 +86,8 @@ function finishTest() { is(contentWindow.GroupItems.groupItems.length, 1, "Only one group is open"); + contentWindow = null; + activeGroup = null; finish(); } @@ -95,29 +98,3 @@ function whenGroupClosed(group, callback) { callback(); }); } - -// ---------- -function whenDialogOpened(callback) { - let wm = Cc["@mozilla.org/appshell/window-mediator;1"] - .getService(Ci.nsIWindowMediator); - - let listener = { - onCloseWindow: function () {}, - onWindowTitleChange: function () {}, - - onOpenWindow: function (xulWin) { - let domWin = xulWin.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); - - whenWindowLoaded(domWin, function () { - let dialog = domWin.document.querySelector("dialog"); - if (dialog) { - wm.removeListener(listener); - callback(dialog); - } - }); - } - }; - - wm.addListener(listener); -} diff --git a/browser/components/tabview/test/browser_tabview_bug649006.js b/browser/components/tabview/test/browser_tabview_bug649006.js index 6ec546d5382..2b8daaa97a8 100644 --- a/browser/components/tabview/test/browser_tabview_bug649006.js +++ b/browser/components/tabview/test/browser_tabview_bug649006.js @@ -81,6 +81,11 @@ function test4() { hideTabView(function() { is(gBrowser.tabs.length, 1, "Total number of tabs is 1 after all tests"); + + contentWindow = null; + contentElement = null; + groupItem = null; + finish(); }); } diff --git a/browser/components/tabview/test/head.js b/browser/components/tabview/test/head.js index 727d41318d3..d9ded0b960c 100644 --- a/browser/components/tabview/test/head.js +++ b/browser/components/tabview/test/head.js @@ -1,6 +1,8 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + // Some tests here assume that all restored tabs are loaded without waiting for // the user to bring them to the foreground. We ensure this by resetting the // related preference (see the "firefox.js" defaults file for details). @@ -418,3 +420,17 @@ function promiseWindowClosed(win) { win.close(); return deferred.promise; } + +// ---------- +function waitForOnBeforeUnloadDialog(browser, callback) { + browser.addEventListener("DOMWillOpenModalDialog", function onModalDialog() { + browser.removeEventListener("DOMWillOpenModalDialog", onModalDialog, true); + + executeSoon(() => { + let stack = browser.parentNode; + let dialogs = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"); + let {button0, button1} = dialogs[0].ui; + callback(button0, button1); + }); + }, true); +} diff --git a/browser/components/tabview/ui.js b/browser/components/tabview/ui.js index c86649dc401..2e653d3f608 100644 --- a/browser/components/tabview/ui.js +++ b/browser/components/tabview/ui.js @@ -527,11 +527,15 @@ let UI = { if (!this.isTabViewVisible() || this._isChangingVisibility) return; - this._isChangingVisibility = true; - // another tab might be select if user decides to stay on a page when // a onclose confirmation prompts. GroupItems.removeHiddenGroups(); + + // We need to set this after removing the hidden groups because doing so + // might show prompts which will cause us to be called again, and we'd get + // stuck if we prevent re-entrancy before doing that. + this._isChangingVisibility = true; + TabItems.pausePainting(); this._reorderTabsOnHide.forEach(function(groupItem) { diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm index befdb4aefc5..0515d4da636 100644 --- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -317,7 +317,7 @@ DevTools.prototype = { closeToolbox: function DT_closeToolbox(target) { let toolbox = this._toolboxes.get(target); if (toolbox == null) { - return; + return promise.reject(null); } return toolbox.destroy(); }, diff --git a/browser/devtools/layoutview/test/browser_editablemodel.js b/browser/devtools/layoutview/test/browser_editablemodel.js index 472c807ba6e..175148df208 100644 --- a/browser/devtools/layoutview/test/browser_editablemodel.js +++ b/browser/devtools/layoutview/test/browser_editablemodel.js @@ -22,7 +22,8 @@ let test = asyncTest(function*() { inspector.sidebar.select("layoutview"); yield inspector.sidebar.once("layoutview-ready"); yield runTests(); - yield gDevTools.closeToolbox(toolbox); + // TODO: Closing the toolbox in this test leaks - bug 994314 + // yield gDevTools.closeToolbox(target); }); addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes", diff --git a/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js b/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js index 82ea704cdcd..136a2fb4ef4 100644 --- a/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js +++ b/browser/devtools/layoutview/test/browser_editablemodel_allproperties.js @@ -1,3 +1,4 @@ + /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ @@ -22,7 +23,8 @@ let test = asyncTest(function*() { inspector.sidebar.select("layoutview"); yield inspector.sidebar.once("layoutview-ready"); yield runTests(); - yield gDevTools.closeToolbox(toolbox); + // TODO: Closing the toolbox in this test leaks - bug 994314 + // yield gDevTools.closeToolbox(target); }); addTest("When all properties are set on the node editing one should work", diff --git a/browser/devtools/layoutview/test/browser_editablemodel_border.js b/browser/devtools/layoutview/test/browser_editablemodel_border.js index 9f08fce2895..e98ba97026d 100644 --- a/browser/devtools/layoutview/test/browser_editablemodel_border.js +++ b/browser/devtools/layoutview/test/browser_editablemodel_border.js @@ -22,7 +22,8 @@ let test = asyncTest(function*() { inspector.sidebar.select("layoutview"); yield inspector.sidebar.once("layoutview-ready"); yield runTests(); - yield gDevTools.closeToolbox(toolbox); + // TODO: Closing the toolbox in this test leaks - bug 994314 + // yield gDevTools.closeToolbox(target); }); addTest("Test that adding a border applies a border style when necessary", diff --git a/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js b/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js index dad50221336..dece035b46d 100644 --- a/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js +++ b/browser/devtools/layoutview/test/browser_editablemodel_stylerules.js @@ -22,7 +22,8 @@ let test = asyncTest(function*() { inspector.sidebar.select("layoutview"); yield inspector.sidebar.once("layoutview-ready"); yield runTests(); - yield gDevTools.closeToolbox(toolbox); + // TODO: Closing the toolbox in this test leaks - bug 994314 + // yield gDevTools.closeToolbox(target); }); addTest("Test that entering units works", diff --git a/browser/devtools/scratchpad/scratchpad.xul b/browser/devtools/scratchpad/scratchpad.xul index f3bd4d0f9f9..da8d863605b 100644 --- a/browser/devtools/scratchpad/scratchpad.xul +++ b/browser/devtools/scratchpad/scratchpad.xul @@ -184,6 +184,34 @@ +

+ + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - diff --git a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js index f1f64b85f2b..17e2ae4ab09 100644 --- a/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js +++ b/browser/devtools/shared/test/browser_toolbar_webconsole_errors_count.js @@ -193,10 +193,11 @@ function test() { function testEnd() { document.getElementById("developer-toolbar-closebutton").doCommand(); let target1 = TargetFactory.forTab(tab1); - gDevTools.closeToolbox(target1); - gBrowser.removeTab(tab1); - gBrowser.removeTab(tab2); - finish(); + gDevTools.closeToolbox(target1).then(() => { + gBrowser.removeTab(tab1); + gBrowser.removeTab(tab2); + finish(); + }); } // Utility functions diff --git a/browser/experiments/Experiments.jsm b/browser/experiments/Experiments.jsm index 6cf71bd8b2f..5deb3504915 100644 --- a/browser/experiments/Experiments.jsm +++ b/browser/experiments/Experiments.jsm @@ -356,6 +356,7 @@ Experiments.Experiments.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsIObserver]), init: function () { + this._shutdown = false; configureLogging(); gExperimentsEnabled = gPrefs.get(PREF_ENABLED, false); @@ -386,10 +387,19 @@ Experiments.Experiments.prototype = { }, /** + * Uninitialize this instance. + * + * This function is susceptible to race conditions. If it is called multiple + * times before the previous uninit() has completed or if it is called while + * an init() operation is being performed, the object may get in bad state + * and/or deadlock could occur. + * * @return Promise<> * The promise is fulfilled when all pending tasks are finished. */ - uninit: function () { + uninit: Task.async(function* () { + yield this._loadTask; + if (!this._shutdown) { this._stopWatchingAddons(); @@ -406,10 +416,11 @@ Experiments.Experiments.prototype = { this._shutdown = true; if (this._mainTask) { - return this._mainTask; + yield this._mainTask; } - return Promise.resolve(); - }, + + this._log.info("Completed uninitialization."); + }), _startWatchingAddons: function () { AddonManager.addAddonListener(this); diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn index 1bedc1682c3..fc07b3f3af1 100644 --- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -135,12 +135,18 @@ browser.jar: #endif * skin/classic/browser/preferences/preferences.css (preferences/preferences.css) * skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css) - skin/classic/browser/preferences/in-content/check.png (preferences/in-content/check.png) - skin/classic/browser/preferences/in-content/icons.png (preferences/in-content/icons.png) - skin/classic/browser/preferences/in-content/header.png (preferences/in-content/header.png) - skin/classic/browser/preferences/in-content/dropdown.png (preferences/in-content/dropdown.png) - skin/classic/browser/preferences/in-content/sorter.png (preferences/in-content/sorter.png) - skin/classic/browser/preferences/in-content/dropdown-disabled.png (preferences/in-content/dropdown-disabled.png) + skin/classic/browser/preferences/in-content/check.png (../shared/incontentprefs/check.png) + skin/classic/browser/preferences/in-content/check@2x.png (../shared/incontentprefs/check@2x.png) + skin/classic/browser/preferences/in-content/icons.png (../shared/incontentprefs/icons.png) + skin/classic/browser/preferences/in-content/icons@2x.png (../shared/incontentprefs/icons@2x.png) + skin/classic/browser/preferences/in-content/header.png (../shared/incontentprefs/header.png) + skin/classic/browser/preferences/in-content/header@2x.png (../shared/incontentprefs/header@2x.png) + skin/classic/browser/preferences/in-content/dropdown.png (../shared/incontentprefs/dropdown.png) + skin/classic/browser/preferences/in-content/dropdown@2x.png (../shared/incontentprefs/dropdown@2x.png) + skin/classic/browser/preferences/in-content/sorter.png (../shared/incontentprefs/sorter.png) + skin/classic/browser/preferences/in-content/sorter@2x.png (../shared/incontentprefs/sorter@2x.png) + skin/classic/browser/preferences/in-content/dropdown-disabled.png (../shared/incontentprefs/dropdown-disabled.png) + skin/classic/browser/preferences/in-content/dropdown-disabled@2x.png (../shared/incontentprefs/dropdown-disabled@2x.png) skin/classic/browser/preferences/applications.css (preferences/applications.css) skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css) skin/classic/browser/social/services-16.png (social/services-16.png) diff --git a/browser/themes/linux/preferences/in-content/dropdown-disabled.png b/browser/themes/linux/preferences/in-content/dropdown-disabled.png deleted file mode 100644 index 9e0a4718265..00000000000 Binary files a/browser/themes/linux/preferences/in-content/dropdown-disabled.png and /dev/null differ diff --git a/browser/themes/linux/preferences/in-content/dropdown.png b/browser/themes/linux/preferences/in-content/dropdown.png deleted file mode 100644 index 9843b82f473..00000000000 Binary files a/browser/themes/linux/preferences/in-content/dropdown.png and /dev/null differ diff --git a/browser/themes/linux/preferences/in-content/header.png b/browser/themes/linux/preferences/in-content/header.png deleted file mode 100644 index de33b7fb4f6..00000000000 Binary files a/browser/themes/linux/preferences/in-content/header.png and /dev/null differ diff --git a/browser/themes/linux/preferences/in-content/icons.png b/browser/themes/linux/preferences/in-content/icons.png deleted file mode 100644 index cf6a16a5198..00000000000 Binary files a/browser/themes/linux/preferences/in-content/icons.png and /dev/null differ diff --git a/browser/themes/linux/preferences/in-content/preferences.css b/browser/themes/linux/preferences/in-content/preferences.css index c79fadb6db6..6c06a2dddb0 100644 --- a/browser/themes/linux/preferences/in-content/preferences.css +++ b/browser/themes/linux/preferences/in-content/preferences.css @@ -2,7 +2,7 @@ - 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/. */ -%include ../../../shared/in-content/preferences.css +%include ../../../shared/incontentprefs/preferences.css button > .button-box, menulist > .menulist-label-box { diff --git a/browser/themes/linux/preferences/in-content/sorter.png b/browser/themes/linux/preferences/in-content/sorter.png deleted file mode 100644 index 3e5661fe169..00000000000 Binary files a/browser/themes/linux/preferences/in-content/sorter.png and /dev/null differ diff --git a/browser/themes/linux/preferences/preferences.css b/browser/themes/linux/preferences/preferences.css index 751a4fd2362..b4909f4a3ba 100644 --- a/browser/themes/linux/preferences/preferences.css +++ b/browser/themes/linux/preferences/preferences.css @@ -169,4 +169,8 @@ label.small { margin: 0; } +#fxa-pweng-help-link > image { + list-style-image: url("chrome://global/skin/icons/question-16.png"); +} + %endif diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn index acf0c47eb12..9b98a267b8a 100644 --- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -223,14 +223,18 @@ browser.jar: skin/classic/browser/preferences/saveFile.png (preferences/saveFile.png) * skin/classic/browser/preferences/preferences.css (preferences/preferences.css) * skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css) - skin/classic/browser/preferences/in-content/check.png (preferences/in-content/check.png) - skin/classic/browser/preferences/in-content/check@2x.png (preferences/in-content/check@2x.png) - skin/classic/browser/preferences/in-content/icons.png (preferences/in-content/icons.png) - skin/classic/browser/preferences/in-content/icons@2x.png (preferences/in-content/icons@2x.png) - skin/classic/browser/preferences/in-content/header.png (preferences/in-content/icons@2x.png) - skin/classic/browser/preferences/in-content/sorter.png (preferences/in-content/sorter.png) - skin/classic/browser/preferences/in-content/dropdown.png (preferences/in-content/dropdown.png) - skin/classic/browser/preferences/in-content/dropdown-disabled.png (preferences/in-content/dropdown-disabled.png) + skin/classic/browser/preferences/in-content/check.png (../shared/incontentprefs/check.png) + skin/classic/browser/preferences/in-content/check@2x.png (../shared/incontentprefs/check@2x.png) + skin/classic/browser/preferences/in-content/icons.png (../shared/incontentprefs/icons.png) + skin/classic/browser/preferences/in-content/icons@2x.png (../shared/incontentprefs/icons@2x.png) + skin/classic/browser/preferences/in-content/header.png (../shared/incontentprefs/header.png) + skin/classic/browser/preferences/in-content/header@2x.png (../shared/incontentprefs/header@2x.png) + skin/classic/browser/preferences/in-content/sorter.png (../shared/incontentprefs/sorter.png) + skin/classic/browser/preferences/in-content/sorter@2x.png (../shared/incontentprefs/sorter@2x.png) + skin/classic/browser/preferences/in-content/dropdown.png (../shared/incontentprefs/dropdown.png) + skin/classic/browser/preferences/in-content/dropdown@2x.png (../shared/incontentprefs/dropdown@2x.png) + skin/classic/browser/preferences/in-content/dropdown-disabled.png (../shared/incontentprefs/dropdown-disabled.png) + skin/classic/browser/preferences/in-content/dropdown-disabled@2x.png (../shared/incontentprefs/dropdown-disabled@2x.png) skin/classic/browser/preferences/applications.css (preferences/applications.css) skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css) skin/classic/browser/social/services-16.png (social/services-16.png) diff --git a/browser/themes/osx/preferences/in-content/check.png b/browser/themes/osx/preferences/in-content/check.png deleted file mode 100644 index a9438df08b4..00000000000 Binary files a/browser/themes/osx/preferences/in-content/check.png and /dev/null differ diff --git a/browser/themes/osx/preferences/in-content/dropdown-disabled.png b/browser/themes/osx/preferences/in-content/dropdown-disabled.png deleted file mode 100644 index 9e0a4718265..00000000000 Binary files a/browser/themes/osx/preferences/in-content/dropdown-disabled.png and /dev/null differ diff --git a/browser/themes/osx/preferences/in-content/dropdown.png b/browser/themes/osx/preferences/in-content/dropdown.png deleted file mode 100644 index 9843b82f473..00000000000 Binary files a/browser/themes/osx/preferences/in-content/dropdown.png and /dev/null differ diff --git a/browser/themes/osx/preferences/in-content/icons.png b/browser/themes/osx/preferences/in-content/icons.png deleted file mode 100644 index cf6a16a5198..00000000000 Binary files a/browser/themes/osx/preferences/in-content/icons.png and /dev/null differ diff --git a/browser/themes/osx/preferences/in-content/icons@2x.png b/browser/themes/osx/preferences/in-content/icons@2x.png deleted file mode 100644 index e85db40ad87..00000000000 Binary files a/browser/themes/osx/preferences/in-content/icons@2x.png and /dev/null differ diff --git a/browser/themes/osx/preferences/in-content/preferences.css b/browser/themes/osx/preferences/in-content/preferences.css index 997e482f762..bf0225eba81 100644 --- a/browser/themes/osx/preferences/in-content/preferences.css +++ b/browser/themes/osx/preferences/in-content/preferences.css @@ -2,7 +2,7 @@ - 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/. */ -%include ../../../shared/in-content/preferences.css +%include ../../../shared/incontentprefs/preferences.css menulist:not([editable="true"]) > .menulist-dropmarker { display: -moz-box; @@ -61,75 +61,3 @@ description { font-size: 1.25rem; line-height: 22px; } - -@media (min-resolution: 2dppx) { - checkbox:hover::before, - checkbox[checked]::before { - background-size: cover; - background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 30, 30, 0); - } - - checkbox[checked]::before { - background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 60, 30, 30); - } - - .category-icon { - list-style-image: url("chrome://browser/skin/preferences/in-content/icons@2x.png"); - } - - #category-general > .category-icon { - -moz-image-region: rect(0, 48px, 48px, 0); - } - - #category-general[selected] > .category-icon { - -moz-image-region: rect(48px, 48px, 96px, 0); - } - - #category-content > .category-icon { - -moz-image-region: rect(0, 96px, 48px, 48px); - } - - #category-content[selected] > .category-icon { - -moz-image-region: rect(48px, 96px, 96px, 48px); - } - - #category-application > .category-icon { - -moz-image-region: rect(0, 144px, 48px, 96px); - } - - #category-application[selected] > .category-icon { - -moz-image-region: rect(48px, 144px, 96px, 96px); - } - - #category-privacy > .category-icon { - -moz-image-region: rect(0, 192px, 48px, 144px); - } - - #category-privacy[selected] > .category-icon { - -moz-image-region: rect(48px, 192px, 96px, 144px); - } - - #category-security > .category-icon { - -moz-image-region: rect(0, 240px, 48px, 192px); - } - - #category-security[selected] > .category-icon { - -moz-image-region: rect(48px, 240px, 96px, 192px); - } - - #category-sync > .category-icon { - -moz-image-region: rect(0, 288px, 48px, 240px); - } - - #category-sync[selected] > .category-icon { - -moz-image-region: rect(48px, 288px, 96px, 240px); - } - - #category-advanced > .category-icon { - -moz-image-region: rect(0, 336px, 48px, 288px); - } - - #category-advanced[selected] > .category-icon { - -moz-image-region: rect(48px, 336px, 96px, 288px); - } -} diff --git a/browser/themes/osx/preferences/in-content/sorter.png b/browser/themes/osx/preferences/in-content/sorter.png deleted file mode 100644 index 3e5661fe169..00000000000 Binary files a/browser/themes/osx/preferences/in-content/sorter.png and /dev/null differ diff --git a/browser/themes/osx/preferences/preferences.css b/browser/themes/osx/preferences/preferences.css index 279859de072..4554be0c694 100644 --- a/browser/themes/osx/preferences/preferences.css +++ b/browser/themes/osx/preferences/preferences.css @@ -234,4 +234,16 @@ html|a.inline-link:-moz-focusring { margin: 0; } +#fxa-pweng-help-link > image { + width: 16px; + height: 16px; + list-style-image: url("chrome://global/skin/icons/question-16.png"); +} + +@media (min-resolution: 2dppx) { + #fxa-pweng-help-link > image { + list-style-image: url("chrome://global/skin/icons/question-32.png"); + } +} + %endif diff --git a/browser/themes/shared/customizableui/panelUIOverlay.inc.css b/browser/themes/shared/customizableui/panelUIOverlay.inc.css index 19b1f8d447f..2bbfe752c9b 100644 --- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css +++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css @@ -244,7 +244,6 @@ panelview:not([mainview]) .toolbarbutton-text, toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"]:not(.panel-wide-item), .panelUI-grid .toolbarbutton-1, -toolbarpaletteitem[place="panel"]:not([haswideitem=true]), .panel-customization-placeholder-child { -moz-appearance: none; -moz-box-orient: vertical; @@ -772,17 +771,8 @@ menuitem.panel-subview-footer@menuStateActive@, } #BMB_bookmarksPopup menupopup > .bookmarks-actions-menuseparator { - /* Hide bottom separator as the styled footer includes a top border serving the same purpose. - * We can't just use display: none here, otherwise scrollbox.xml will flip out and sometimes - * refuse to scroll for us (see bug 984156). Instead, we set it to visibility hidden, force - * a minimum height, and then negative-margin that single pixel into oblivion. That seems - * to be enough to make scrollbox happy. - */ - -moz-appearance: none; - visibility: hidden; - min-height: 1px; - margin: -1px 0 0; - border: none; + /* Hide bottom separator as the styled footer includes a top border serving the same purpose */ + display: none; } /* Popups with only one item don't have a footer */ @@ -918,10 +908,14 @@ toolbarpaletteitem[place="palette"] > #search-container { /* Make direct siblings overlap borders: */ .toolbaritem-combined-buttons + .toolbaritem-combined-buttons@inAnyPanel@ { - margin-top: -1px; border-top-color: transparent !important; } +.toolbaritem-combined-buttons + .toolbaritem-combined-buttons@inAnyPanel@, +toolbarpaletteitem[haswideitem][place="panel"] + toolbarpaletteitem[haswideitem][place="panel"] { + margin-top: -1px; +} + .toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton { border: 0; padding: .5em; diff --git a/browser/themes/linux/preferences/in-content/check.png b/browser/themes/shared/incontentprefs/check.png similarity index 84% rename from browser/themes/linux/preferences/in-content/check.png rename to browser/themes/shared/incontentprefs/check.png index a9438df08b4..f7560f27235 100644 Binary files a/browser/themes/linux/preferences/in-content/check.png and b/browser/themes/shared/incontentprefs/check.png differ diff --git a/browser/themes/osx/preferences/in-content/check@2x.png b/browser/themes/shared/incontentprefs/check@2x.png similarity index 91% rename from browser/themes/osx/preferences/in-content/check@2x.png rename to browser/themes/shared/incontentprefs/check@2x.png index 6ba07a2a8ff..710ea3f799c 100644 Binary files a/browser/themes/osx/preferences/in-content/check@2x.png and b/browser/themes/shared/incontentprefs/check@2x.png differ diff --git a/browser/themes/shared/incontentprefs/dropdown-disabled.png b/browser/themes/shared/incontentprefs/dropdown-disabled.png new file mode 100644 index 00000000000..d92c55d9765 Binary files /dev/null and b/browser/themes/shared/incontentprefs/dropdown-disabled.png differ diff --git a/browser/themes/shared/incontentprefs/dropdown-disabled@2x.png b/browser/themes/shared/incontentprefs/dropdown-disabled@2x.png new file mode 100644 index 00000000000..1b432f7575c Binary files /dev/null and b/browser/themes/shared/incontentprefs/dropdown-disabled@2x.png differ diff --git a/browser/themes/shared/incontentprefs/dropdown.png b/browser/themes/shared/incontentprefs/dropdown.png new file mode 100644 index 00000000000..2e0c0e9f326 Binary files /dev/null and b/browser/themes/shared/incontentprefs/dropdown.png differ diff --git a/browser/themes/shared/incontentprefs/dropdown@2x.png b/browser/themes/shared/incontentprefs/dropdown@2x.png new file mode 100644 index 00000000000..be56b20bb2f Binary files /dev/null and b/browser/themes/shared/incontentprefs/dropdown@2x.png differ diff --git a/browser/themes/shared/incontentprefs/header.png b/browser/themes/shared/incontentprefs/header.png new file mode 100644 index 00000000000..ce05ed2792b Binary files /dev/null and b/browser/themes/shared/incontentprefs/header.png differ diff --git a/browser/themes/shared/incontentprefs/header@2x.png b/browser/themes/shared/incontentprefs/header@2x.png new file mode 100644 index 00000000000..83e8868ace7 Binary files /dev/null and b/browser/themes/shared/incontentprefs/header@2x.png differ diff --git a/browser/themes/shared/incontentprefs/icons.png b/browser/themes/shared/incontentprefs/icons.png new file mode 100644 index 00000000000..af5a18697b0 Binary files /dev/null and b/browser/themes/shared/incontentprefs/icons.png differ diff --git a/browser/themes/shared/incontentprefs/icons@2x.png b/browser/themes/shared/incontentprefs/icons@2x.png new file mode 100644 index 00000000000..a4cbde79e94 Binary files /dev/null and b/browser/themes/shared/incontentprefs/icons@2x.png differ diff --git a/browser/themes/shared/in-content/preferences.css b/browser/themes/shared/incontentprefs/preferences.css similarity index 78% rename from browser/themes/shared/in-content/preferences.css rename to browser/themes/shared/incontentprefs/preferences.css index 890108ac49f..c6af4373970 100644 --- a/browser/themes/shared/in-content/preferences.css +++ b/browser/themes/shared/incontentprefs/preferences.css @@ -178,10 +178,10 @@ button[type="menu"] > .button-box > .button-menu-dropmarker { -moz-margin-start: 10px; padding: 0; width: 10px; - height: 15px; + height: 16px; border: none; background-color: transparent; - list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown.png") + list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown.png"); } .spinbuttons-button { @@ -232,6 +232,23 @@ menulist[disabled="true"]:not([editable="true"]) > .menulist-dropmarker { list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown-disabled.png") } +@media (min-resolution: 2dppx) { + menulist:not([editable="true"]) > .menulist-dropmarker, + button[type="menu"] > .button-box > .button-menu-dropmarker { + list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown@2x.png"); + } + + menulist[disabled="true"]:not([editable="true"]) > .menulist-dropmarker { + list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown-disabled@2x.png") + } + + menulist:not([editable="true"]) > .menulist-dropmarker > .dropmarker-icon, + button[type="menu"] > .button-box > .button-menu-dropmarker > .dropmarker-icon { + width: 10px; + height: 16px; + } +} + menulist > menupopup, button[type="menu"] > menupopup { -moz-appearance: none; @@ -365,6 +382,18 @@ checkbox[checked]::before { background-position: -15px 0; } +@media (min-resolution: 2dppx) { + checkbox:hover::before { + background-size: cover; + background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 30, 30, 0); + } + + checkbox[checked]::before { + background-size: cover; + background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 60, 30, 30); + } +} + .radio-check { -moz-appearance: none; width: 23px; @@ -509,6 +538,68 @@ radio[selected]::before { -moz-image-region: rect(24px, 168px, 48px, 144px); } +@media (min-resolution: 2dppx) { + .category-icon { + list-style-image: url("chrome://browser/skin/preferences/in-content/icons@2x.png"); + } + + #category-general > .category-icon { + -moz-image-region: rect(0, 48px, 48px, 0); + } + + #category-general[selected] > .category-icon { + -moz-image-region: rect(48px, 48px, 96px, 0); + } + + #category-content > .category-icon { + -moz-image-region: rect(0, 96px, 48px, 48px); + } + + #category-content[selected] > .category-icon { + -moz-image-region: rect(48px, 96px, 96px, 48px); + } + + #category-application > .category-icon { + -moz-image-region: rect(0, 144px, 48px, 96px); + } + + #category-application[selected] > .category-icon { + -moz-image-region: rect(48px, 144px, 96px, 96px); + } + + #category-privacy > .category-icon { + -moz-image-region: rect(0, 192px, 48px, 144px); + } + + #category-privacy[selected] > .category-icon { + -moz-image-region: rect(48px, 192px, 96px, 144px); + } + + #category-security > .category-icon { + -moz-image-region: rect(0, 240px, 48px, 192px); + } + + #category-security[selected] > .category-icon { + -moz-image-region: rect(48px, 240px, 96px, 192px); + } + + #category-sync > .category-icon { + -moz-image-region: rect(0, 288px, 48px, 240px); + } + + #category-sync[selected] > .category-icon { + -moz-image-region: rect(48px, 288px, 96px, 240px); + } + + #category-advanced > .category-icon { + -moz-image-region: rect(0, 336px, 48px, 288px); + } + + #category-advanced[selected] > .category-icon { + -moz-image-region: rect(48px, 336px, 96px, 288px); + } +} + /* header */ .header { @@ -530,31 +621,65 @@ radio[selected]::before { } #header-general > .header-icon { - -moz-image-region: rect(3px, 45px, 45px, 3px); + -moz-image-region: rect(0, 40px, 40px, 0); } #header-content > .header-icon { - -moz-image-region: rect(3px, 93px, 45px, 51px); + -moz-image-region: rect(0, 80px, 40px, 40px); } #header-application > .header-icon { - -moz-image-region: rect(3px, 141px, 45px, 99px); + -moz-image-region: rect(0, 120px, 40px, 80px); } #header-privacy > .header-icon { - -moz-image-region: rect(3px, 189px, 45px, 147px); + -moz-image-region: rect(0, 160px, 40px, 120px); } #header-security > .header-icon { - -moz-image-region: rect(3px, 237px, 45px, 195px); + -moz-image-region: rect(0, 200px, 40px, 160px); } #header-sync > .header-icon { - -moz-image-region: rect(3px, 285px, 45px, 243px); + -moz-image-region: rect(0, 240px, 40px, 200px); } #header-advanced > .header-icon { - -moz-image-region: rect(3px, 333px, 45px, 291px); + -moz-image-region: rect(0, 280px, 40px, 240px); +} + +@media (min-resolution: 2dppx) { + .header-icon { + list-style-image: url("chrome://browser/skin/preferences/in-content/header@2x.png"); + } + + #header-general > .header-icon { + -moz-image-region: rect(0, 80px, 80px, 0); + } + + #header-content > .header-icon { + -moz-image-region: rect(0, 160px, 80px, 80px); + } + + #header-application > .header-icon { + -moz-image-region: rect(0, 240px, 80px, 160px); + } + + #header-privacy > .header-icon { + -moz-image-region: rect(0, 320px, 80px, 240px); + } + + #header-security > .header-icon { + -moz-image-region: rect(0, 400px, 80px, 320px); + } + + #header-sync > .header-icon { + -moz-image-region: rect(0, 480px, 80px, 400px); + } + + #header-advanced > .header-icon { + -moz-image-region: rect(0, 560px, 80px, 480px); + } } .indent { @@ -645,7 +770,7 @@ filefield { #typeColumn > .treecol-sortdirection[sortDirection=ascending], #actionColumn > .treecol-sortdirection[sortDirection=ascending], #typeColumn > .treecol-sortdirection[sortDirection=descending], -#actionColumn > .treecol-sortdirection[sortDirection=descending] { +#actionColumn > .treecol-sortdirection[sortDirection=descending] { -moz-appearance: none; list-style-image: url("chrome://browser/skin/preferences/in-content/sorter.png"); } @@ -655,6 +780,17 @@ filefield { transform: scaleY(-1); } +@media (min-resolution: 2dppx) { + #typeColumn > .treecol-sortdirection[sortDirection=ascending], + #actionColumn > .treecol-sortdirection[sortDirection=ascending], + #typeColumn > .treecol-sortdirection[sortDirection=descending], + #actionColumn > .treecol-sortdirection[sortDirection=descending] { + width: 12px; + height: 8px; + list-style-image: url("chrome://browser/skin/preferences/in-content/sorter@2x.png"); + } +} + #handlersView > richlistitem { min-height: 40px !important; } diff --git a/browser/themes/shared/incontentprefs/sorter.png b/browser/themes/shared/incontentprefs/sorter.png new file mode 100644 index 00000000000..41934733aae Binary files /dev/null and b/browser/themes/shared/incontentprefs/sorter.png differ diff --git a/browser/themes/shared/incontentprefs/sorter@2x.png b/browser/themes/shared/incontentprefs/sorter@2x.png new file mode 100644 index 00000000000..5e4d46d4c4c Binary files /dev/null and b/browser/themes/shared/incontentprefs/sorter@2x.png differ diff --git a/browser/themes/windows/browser-aero.css b/browser/themes/windows/browser-aero.css index 0adbe0f3610..99969c748ef 100644 --- a/browser/themes/windows/browser-aero.css +++ b/browser/themes/windows/browser-aero.css @@ -58,8 +58,6 @@ border-color: hsla(210,54%,20%,.25) hsla(210,54%,20%,.27) hsla(210,54%,20%,.3); box-shadow: 0 1px 0 hsla(0,0%,0%,.01) inset, 0 1px 0 hsla(0,0%,100%,.1); - transition-property: border-color; - transition-duration: 200ms; } #urlbar:not(:-moz-lwtheme)[focused], diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn index 461bd6d694a..44e2d710f4b 100644 --- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -160,14 +160,18 @@ browser.jar: skin/classic/browser/preferences/saveFile.png (preferences/saveFile.png) * skin/classic/browser/preferences/preferences.css (preferences/preferences.css) * skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css) - skin/classic/browser/preferences/in-content/check.png (preferences/in-content/check.png) - skin/classic/browser/preferences/in-content/check@2x.png (preferences/in-content/check@2x.png) - skin/classic/browser/preferences/in-content/icons.png (preferences/in-content/icons.png) - skin/classic/browser/preferences/in-content/icons@2x.png (preferences/in-content/icons@2x.png) - skin/classic/browser/preferences/in-content/header.png (preferences/in-content/icons@2x.png) - skin/classic/browser/preferences/in-content/sorter.png (preferences/in-content/sorter.png) - skin/classic/browser/preferences/in-content/dropdown.png (preferences/in-content/dropdown.png) - skin/classic/browser/preferences/in-content/dropdown-disabled.png (preferences/in-content/dropdown-disabled.png) + skin/classic/browser/preferences/in-content/check.png (../shared/incontentprefs/check.png) + skin/classic/browser/preferences/in-content/check@2x.png (../shared/incontentprefs/check@2x.png) + skin/classic/browser/preferences/in-content/icons.png (../shared/incontentprefs/icons.png) + skin/classic/browser/preferences/in-content/icons@2x.png (../shared/incontentprefs/icons@2x.png) + skin/classic/browser/preferences/in-content/header.png (../shared/incontentprefs/header.png) + skin/classic/browser/preferences/in-content/header@2x.png (../shared/incontentprefs/header@2x.png) + skin/classic/browser/preferences/in-content/sorter.png (../shared/incontentprefs/sorter.png) + skin/classic/browser/preferences/in-content/sorter@2x.png (../shared/incontentprefs/sorter@2x.png) + skin/classic/browser/preferences/in-content/dropdown.png (../shared/incontentprefs/dropdown.png) + skin/classic/browser/preferences/in-content/dropdown@2x.png (../shared/incontentprefs/dropdown@2x.png) + skin/classic/browser/preferences/in-content/dropdown-disabled.png (../shared/incontentprefs/dropdown-disabled.png) + skin/classic/browser/preferences/in-content/dropdown-disabled@2x.png (../shared/incontentprefs/dropdown-disabled@2x.png) skin/classic/browser/preferences/applications.css (preferences/applications.css) skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css) skin/classic/browser/social/services-16.png (social/services-16.png) @@ -511,14 +515,18 @@ browser.jar: skin/classic/aero/browser/preferences/saveFile.png (preferences/saveFile-aero.png) * skin/classic/aero/browser/preferences/preferences.css (preferences/preferences.css) * skin/classic/aero/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css) - skin/classic/aero/browser/preferences/in-content/check.png (preferences/in-content/check.png) - skin/classic/aero/browser/preferences/in-content/check@2x.png (preferences/in-content/check@2x.png) - skin/classic/aero/browser/preferences/in-content/icons.png (preferences/in-content/icons.png) - skin/classic/aero/browser/preferences/in-content/icons@2x.png (preferences/in-content/icons@2x.png) - skin/classic/aero/browser/preferences/in-content/header.png (preferences/in-content/icons@2x.png) - skin/classic/aero/browser/preferences/in-content/sorter.png (preferences/in-content/sorter.png) - skin/classic/aero/browser/preferences/in-content/dropdown.png (preferences/in-content/dropdown.png) - skin/classic/aero/browser/preferences/in-content/dropdown-disabled.png (preferences/in-content/dropdown-disabled.png) + skin/classic/aero/browser/preferences/in-content/check.png (../shared/incontentprefs/check.png) + skin/classic/aero/browser/preferences/in-content/check@2x.png (../shared/incontentprefs/check@2x.png) + skin/classic/aero/browser/preferences/in-content/icons.png (../shared/incontentprefs/icons.png) + skin/classic/aero/browser/preferences/in-content/icons@2x.png (../shared/incontentprefs/icons@2x.png) + skin/classic/aero/browser/preferences/in-content/header.png (../shared/incontentprefs/header.png) + skin/classic/aero/browser/preferences/in-content/header@2x.png (../shared/incontentprefs/header@2x.png) + skin/classic/aero/browser/preferences/in-content/sorter.png (../shared/incontentprefs/sorter.png) + skin/classic/aero/browser/preferences/in-content/sorter@2x.png (../shared/incontentprefs/sorter@2x.png) + skin/classic/aero/browser/preferences/in-content/dropdown.png (../shared/incontentprefs/dropdown.png) + skin/classic/aero/browser/preferences/in-content/dropdown@2x.png (../shared/incontentprefs/dropdown@2x.png) + skin/classic/aero/browser/preferences/in-content/dropdown-disabled.png (../shared/incontentprefs/dropdown-disabled.png) + skin/classic/aero/browser/preferences/in-content/dropdown-disabled@2x.png (../shared/incontentprefs/dropdown-disabled@2x.png) skin/classic/aero/browser/preferences/applications.css (preferences/applications.css) skin/classic/aero/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css) skin/classic/aero/browser/social/services-16.png (social/services-16.png) diff --git a/browser/themes/windows/preferences/in-content/check.png b/browser/themes/windows/preferences/in-content/check.png deleted file mode 100644 index a9438df08b4..00000000000 Binary files a/browser/themes/windows/preferences/in-content/check.png and /dev/null differ diff --git a/browser/themes/windows/preferences/in-content/check@2x.png b/browser/themes/windows/preferences/in-content/check@2x.png deleted file mode 100644 index 6ba07a2a8ff..00000000000 Binary files a/browser/themes/windows/preferences/in-content/check@2x.png and /dev/null differ diff --git a/browser/themes/windows/preferences/in-content/dropdown-disabled.png b/browser/themes/windows/preferences/in-content/dropdown-disabled.png deleted file mode 100644 index 9e0a4718265..00000000000 Binary files a/browser/themes/windows/preferences/in-content/dropdown-disabled.png and /dev/null differ diff --git a/browser/themes/windows/preferences/in-content/dropdown.png b/browser/themes/windows/preferences/in-content/dropdown.png deleted file mode 100644 index 9843b82f473..00000000000 Binary files a/browser/themes/windows/preferences/in-content/dropdown.png and /dev/null differ diff --git a/browser/themes/windows/preferences/in-content/icons.png b/browser/themes/windows/preferences/in-content/icons.png deleted file mode 100644 index cf6a16a5198..00000000000 Binary files a/browser/themes/windows/preferences/in-content/icons.png and /dev/null differ diff --git a/browser/themes/windows/preferences/in-content/icons@2x.png b/browser/themes/windows/preferences/in-content/icons@2x.png deleted file mode 100644 index e85db40ad87..00000000000 Binary files a/browser/themes/windows/preferences/in-content/icons@2x.png and /dev/null differ diff --git a/browser/themes/windows/preferences/in-content/preferences.css b/browser/themes/windows/preferences/in-content/preferences.css index abb831a5036..d09abe98f69 100644 --- a/browser/themes/windows/preferences/in-content/preferences.css +++ b/browser/themes/windows/preferences/in-content/preferences.css @@ -2,7 +2,7 @@ - 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/. */ -%include ../../../shared/in-content/preferences.css +%include ../../../shared/incontentprefs/preferences.css menulist:not([editable="true"]) > .menulist-dropmarker { margin-top: 1px; @@ -30,75 +30,3 @@ radio[selected]::before { .actionsMenu > .menulist-label-box > .menulist-icon { -moz-margin-end: 9px; } - -@media (min-resolution: 2dppx) { - checkbox:hover::before, - checkbox[checked]::before { - background-size: cover; - background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 30, 30, 0); - } - - checkbox[checked]::before { - background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 60, 30, 30); - } - - .category-icon { - list-style-image: url("chrome://browser/skin/preferences/in-content/icons@2x.png"); - } - - #category-general > .category-icon { - -moz-image-region: rect(0, 48px, 48px, 0); - } - - #category-general[selected] > .category-icon { - -moz-image-region: rect(48px, 48px, 96px, 0); - } - - #category-content > .category-icon { - -moz-image-region: rect(0, 96px, 48px, 48px); - } - - #category-content[selected] > .category-icon { - -moz-image-region: rect(48px, 96px, 96px, 48px); - } - - #category-application > .category-icon { - -moz-image-region: rect(0, 144px, 48px, 96px); - } - - #category-application[selected] > .category-icon { - -moz-image-region: rect(48px, 144px, 96px, 96px); - } - - #category-privacy > .category-icon { - -moz-image-region: rect(0, 192px, 48px, 144px); - } - - #category-privacy[selected] > .category-icon { - -moz-image-region: rect(48px, 192px, 96px, 144px); - } - - #category-security > .category-icon { - -moz-image-region: rect(0, 240px, 48px, 192px); - } - - #category-security[selected] > .category-icon { - -moz-image-region: rect(48px, 240px, 96px, 192px); - } - - #category-sync > .category-icon { - -moz-image-region: rect(0, 288px, 48px, 240px); - } - - #category-sync[selected] > .category-icon { - -moz-image-region: rect(48px, 288px, 96px, 240px); - } - - #category-advanced > .category-icon { - -moz-image-region: rect(0, 336px, 48px, 288px); - } - - #category-advanced[selected] > .category-icon { - -moz-image-region: rect(48px, 336px, 96px, 288px); - } -} diff --git a/browser/themes/windows/preferences/in-content/sorter.png b/browser/themes/windows/preferences/in-content/sorter.png deleted file mode 100644 index 3e5661fe169..00000000000 Binary files a/browser/themes/windows/preferences/in-content/sorter.png and /dev/null differ diff --git a/browser/themes/windows/preferences/preferences.css b/browser/themes/windows/preferences/preferences.css index 79574eecea7..ecaa8d21d49 100644 --- a/browser/themes/windows/preferences/preferences.css +++ b/browser/themes/windows/preferences/preferences.css @@ -159,4 +159,8 @@ label.small { margin: 0; } +#fxa-pweng-help-link > image { + list-style-image: url("chrome://global/skin/icons/question-16.png"); +} + %endif diff --git a/content/xul/document/src/XULDocument.cpp b/content/xul/document/src/XULDocument.cpp index 3f5d5a957c6..e09b8456f8f 100644 --- a/content/xul/document/src/XULDocument.cpp +++ b/content/xul/document/src/XULDocument.cpp @@ -2485,6 +2485,8 @@ XULDocument::PrepareToWalk() // Block onload until we've finished building the complete // document content model. BlockOnload(); + + nsContentSink::NotifyDocElementCreated(this); } // There'd better not be anything on the context stack at this diff --git a/content/xul/document/test/chrome.ini b/content/xul/document/test/chrome.ini index 2c2c5f4b5c4..f9c46774385 100644 --- a/content/xul/document/test/chrome.ini +++ b/content/xul/document/test/chrome.ini @@ -5,6 +5,7 @@ support-files = overlay2_bug335375.xul window_bug583948.xul window_bug757137.xul + window_documentnotification.xul [test_bug199692.xul] [test_bug311681.xul] @@ -20,3 +21,4 @@ support-files = [test_bug583948.xul] [test_bug640158_overlay_persist.xul] [test_bug757137.xul] +[test_documentnotification.xul] diff --git a/content/xul/document/test/test_documentnotification.xul b/content/xul/document/test/test_documentnotification.xul new file mode 100644 index 00000000000..fcd0a5c5ae6 --- /dev/null +++ b/content/xul/document/test/test_documentnotification.xul @@ -0,0 +1,40 @@ + + + + + + + + + diff --git a/content/xul/document/test/window_documentnotification.xul b/content/xul/document/test/window_documentnotification.xul new file mode 100644 index 00000000000..08d081d889b --- /dev/null +++ b/content/xul/document/test/window_documentnotification.xul @@ -0,0 +1,6 @@ + + + + + diff --git a/docshell/base/nsIContentViewer.idl b/docshell/base/nsIContentViewer.idl index bb2e3a89fd7..2f7999c258f 100644 --- a/docshell/base/nsIContentViewer.idl +++ b/docshell/base/nsIContentViewer.idl @@ -27,7 +27,7 @@ class nsDOMNavigationTiming; [ptr] native nsViewPtr(nsView); [ptr] native nsDOMNavigationTimingPtr(nsDOMNavigationTiming); -[scriptable, builtinclass, uuid(4fc2512b-87c7-4a37-9fac-cee0f116bfdf)] +[scriptable, builtinclass, uuid(0cb321bd-5b38-4586-8fcd-d43b366886fb)] interface nsIContentViewer : nsISupports { @@ -49,6 +49,11 @@ interface nsIContentViewer : nsISupports */ boolean permitUnload([optional] in boolean aCallerClosesWindow); + /** + * Exposes whether we're blocked in a call to permitUnload. + */ + readonly attribute boolean inPermitUnload; + /** * As above, but this passes around the aShouldPrompt argument to keep * track of whether the user has responded to a prompt. diff --git a/dom/system/OSFileConstants.cpp b/dom/system/OSFileConstants.cpp index 4b213610e15..1e7edc29e32 100644 --- a/dom/system/OSFileConstants.cpp +++ b/dom/system/OSFileConstants.cpp @@ -326,6 +326,17 @@ void CleanupOSFileConstants() #define INT_CONSTANT(name) \ { #name, INT_TO_JSVAL(name) } +/** + * Define a simple read-only property holding an unsigned integer. + * + * @param name The name of the constant. Used both as the JS name for the + * constant and to access its value. Must be defined. + * + * Produces a |ConstantSpec|. + */ +#define UINT_CONSTANT(name) \ + { #name, UINT_TO_JSVAL((name)) } + /** * End marker for ConstantSpec */ @@ -682,7 +693,7 @@ static const dom::ConstantSpec gWinProperties[] = INT_CONSTANT(FILE_END), // SetFilePointer error constant - INT_CONSTANT(INVALID_SET_FILE_POINTER), + UINT_CONSTANT(INVALID_SET_FILE_POINTER), // File attributes INT_CONSTANT(FILE_ATTRIBUTE_DIRECTORY), diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp index 53e492dec90..4c5a49f189f 100644 --- a/layout/base/nsDocumentViewer.cpp +++ b/layout/base/nsDocumentViewer.cpp @@ -23,6 +23,7 @@ #include "nsStyleSet.h" #include "nsCSSStyleSheet.h" #include "nsIFrame.h" +#include "nsIWritablePropertyBag2.h" #include "nsSubDocumentFrame.h" #include "nsILinkHandler.h" @@ -1158,6 +1159,14 @@ nsDocumentViewer::PermitUnloadInternal(bool aCallerClosesWindow, nsCOMPtr prompt = do_GetInterface(docShell); if (prompt) { + nsCOMPtr promptBag = do_QueryInterface(prompt); + if (promptBag) { + bool isTabModalPromptAllowed; + GetIsTabModalPromptAllowed(&isTabModalPromptAllowed); + promptBag->SetPropertyAsBool(NS_LITERAL_STRING("allowTabModal"), + isTabModalPromptAllowed); + } + nsXPIDLString title, message, stayLabel, leaveLabel; rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, "OnBeforeUnloadTitle", @@ -1200,7 +1209,19 @@ nsDocumentViewer::PermitUnloadInternal(bool aCallerClosesWindow, leaveLabel, stayLabel, nullptr, nullptr, &dummy, &buttonPressed); mInPermitUnloadPrompt = false; - NS_ENSURE_SUCCESS(rv, rv); + + // If the prompt aborted, we tell our consumer that it is not allowed + // to unload the page. One reason that prompts abort is that the user + // performed some action that caused the page to unload while our prompt + // was active. In those cases we don't want our consumer to also unload + // the page. + // + // XXX: Are there other cases where prompts can abort? Is it ok to + // prevent unloading the page in those cases? + if (NS_FAILED(rv)) { + *aPermitUnload = false; + return NS_OK; + } // Button 0 == leave, button 1 == stay *aPermitUnload = (buttonPressed == 0); @@ -1247,6 +1268,13 @@ nsDocumentViewer::GetBeforeUnloadFiring(bool* aInEvent) return NS_OK; } +NS_IMETHODIMP +nsDocumentViewer::GetInPermitUnload(bool* aInEvent) +{ + *aInEvent = mInPermitUnloadPrompt; + return NS_OK; +} + NS_IMETHODIMP nsDocumentViewer::ResetCloseWindow() { @@ -4313,7 +4341,7 @@ nsDocumentViewer::GetHistoryEntry(nsISHEntry **aHistoryEntry) NS_IMETHODIMP nsDocumentViewer::GetIsTabModalPromptAllowed(bool *aAllowed) { - *aAllowed = !(mInPermitUnload || mHidden); + *aAllowed = !mHidden; return NS_OK; } diff --git a/mobile/android/base/DataReportingNotification.java b/mobile/android/base/DataReportingNotification.java index 0c05141b390..682bf8cfe64 100644 --- a/mobile/android/base/DataReportingNotification.java +++ b/mobile/android/base/DataReportingNotification.java @@ -13,11 +13,13 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Resources; import android.graphics.Typeface; import android.os.Build; import android.support.v4.app.NotificationCompat; import android.text.Spannable; import android.text.SpannableString; +import android.text.TextUtils; import android.text.style.StyleSpan; public class DataReportingNotification { @@ -28,15 +30,50 @@ public class DataReportingNotification { private static final String PREFS_POLICY_NOTIFIED_TIME = "datareporting.policy.dataSubmissionPolicyNotifiedTime"; private static final String PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion"; - private static final int DATA_REPORTING_VERSION = 1; + private static final int DATA_REPORTING_VERSION = 2; public static void checkAndNotifyPolicy(Context context) { SharedPreferences dataPrefs = GeckoSharedPrefs.forApp(context); + final int currentVersion = dataPrefs.getInt(PREFS_POLICY_VERSION, -1); - // Notify if user has not been notified or if policy version has changed. - if ((!dataPrefs.contains(PREFS_POLICY_NOTIFIED_TIME)) || - (DATA_REPORTING_VERSION != dataPrefs.getInt(PREFS_POLICY_VERSION, -1))) { + if (currentVersion < 1) { + // This is a first run, so notify user about data policy. + notifyDataPolicy(context, dataPrefs); + // If healthreport is enabled, set default preference value. + if (AppConstants.MOZ_SERVICES_HEALTHREPORT) { + SharedPreferences.Editor editor = dataPrefs.edit(); + editor.putBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true); + editor.commit(); + } + return; + } + + if (currentVersion == 1) { + // Redisplay notification only for Beta because version 2 updates Beta policy and update version. + if (TextUtils.equals("beta", AppConstants.MOZ_UPDATE_CHANNEL)) { + notifyDataPolicy(context, dataPrefs); + } else { + // Silently update the version. + SharedPreferences.Editor editor = dataPrefs.edit(); + editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION); + editor.commit(); + } + return; + } + + if (currentVersion >= DATA_REPORTING_VERSION) { + // Do nothing, we're at a current (or future) version. + return; + } + } + + /** + * Launch a notification of the data policy, and record notification time and version. + */ + private static void notifyDataPolicy(Context context, SharedPreferences sharedPrefs) { + boolean result = false; + try { // Launch main App to launch Data choices when notification is clicked. Intent prefIntent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS); prefIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME); @@ -45,21 +82,22 @@ public class DataReportingNotification { prefIntent.putExtra(ALERT_NAME_DATAREPORTING_NOTIFICATION, true); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT); + final Resources resources = context.getResources(); // Create and send notification. - String notificationTitle = context.getResources().getString(R.string.datareporting_notification_title); + String notificationTitle = resources.getString(R.string.datareporting_notification_title); String notificationSummary; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - notificationSummary = context.getResources().getString(R.string.datareporting_notification_action); + notificationSummary = resources.getString(R.string.datareporting_notification_action); } else { - // Display partial version of Big Style notification for supporting devices. - notificationSummary = context.getResources().getString(R.string.datareporting_notification_summary); + // Display partial version of Big Style notification for supporting devices. + notificationSummary = resources.getString(R.string.datareporting_notification_summary); } - String notificationAction = context.getResources().getString(R.string.datareporting_notification_action); - String notificationBigSummary = context.getResources().getString(R.string.datareporting_notification_summary); + String notificationAction = resources.getString(R.string.datareporting_notification_action); + String notificationBigSummary = resources.getString(R.string.datareporting_notification_summary); // Make styled ticker text for display in notification bar. - String tickerString = context.getResources().getString(R.string.datareporting_notification_ticker_text); + String tickerString = resources.getString(R.string.datareporting_notification_ticker_text); SpannableString tickerText = new SpannableString(tickerString); // Bold the notification title of the ticker text, which is the same string as notificationTitle. tickerText.setSpan(new StyleSpan(Typeface.BOLD), 0, notificationTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); @@ -81,17 +119,16 @@ public class DataReportingNotification { notificationManager.notify(notificationID, notification); // Record version and notification time. - SharedPreferences.Editor editor = dataPrefs.edit(); + SharedPreferences.Editor editor = sharedPrefs.edit(); long now = System.currentTimeMillis(); editor.putLong(PREFS_POLICY_NOTIFIED_TIME, now); editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION); - - // If healthreport is enabled, set default preference value. - if (AppConstants.MOZ_SERVICES_HEALTHREPORT) { - editor.putBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true); - } - editor.commit(); + result = true; + } finally { + // We want to track any errors, so record notification outcome. + final String notificationEvent = TelemetryContract.Event.POLICY_NOTIFICATION_SUCCESS + result; + Telemetry.sendUIEvent(notificationEvent); } } } diff --git a/mobile/android/base/TelemetryContract.java b/mobile/android/base/TelemetryContract.java index 116bf275447..50398999c5c 100644 --- a/mobile/android/base/TelemetryContract.java +++ b/mobile/android/base/TelemetryContract.java @@ -14,7 +14,10 @@ public interface TelemetryContract { * Holds event names. Intended for use with * Telemetry.sendUIEvent() as the "action" parameter. */ - public interface Event {} + public interface Event { + // Outcome of data policy notification: can be true or false. + public static final String POLICY_NOTIFICATION_SUCCESS = "policynotification.success.1:"; + } /** * Holds event methods. Intended for use in diff --git a/mobile/android/base/fxa/login/State.java b/mobile/android/base/fxa/login/State.java index 49d82281543..7ebed7cadf2 100644 --- a/mobile/android/base/fxa/login/State.java +++ b/mobile/android/base/fxa/login/State.java @@ -9,7 +9,7 @@ import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.Utils; public abstract class State { - public static final long CURRENT_VERSION = 1L; + public static final long CURRENT_VERSION = 2L; public enum StateLabel { Engaged, diff --git a/mobile/android/base/fxa/login/StateFactory.java b/mobile/android/base/fxa/login/StateFactory.java index d604f95d2b6..51a14bf122a 100644 --- a/mobile/android/base/fxa/login/StateFactory.java +++ b/mobile/android/base/fxa/login/StateFactory.java @@ -7,18 +7,65 @@ package org.mozilla.gecko.fxa.login; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.DSACryptoImplementation; import org.mozilla.gecko.browserid.RSACryptoImplementation; +import org.mozilla.gecko.fxa.FxAccountConstants; import org.mozilla.gecko.fxa.login.State.StateLabel; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.Utils; +/** + * Create {@link State} instances from serialized representations. + *

+ * Version 1 recognizes 5 state labels (Engaged, Cohabiting, Married, Separated, + * Doghouse). In the Cohabiting and Married states, the associated key pairs are + * always RSA key pairs. + *

+ * Version 2 is identical to version 1, except that in the Cohabiting and + * Married states, the associated keypairs are always DSA key pairs. + */ public class StateFactory { + private static final String LOG_TAG = StateFactory.class.getSimpleName(); + + private static final int KEY_PAIR_SIZE_IN_BITS_V1 = 1024; + + public static BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { + // New key pairs are always DSA. + return DSACryptoImplementation.generateKeyPair(KEY_PAIR_SIZE_IN_BITS_V1); + } + + protected static BrowserIDKeyPair keyPairFromJSONObjectV1(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { + // V1 key pairs are RSA. + return RSACryptoImplementation.fromJSONObject(o); + } + + protected static BrowserIDKeyPair keyPairFromJSONObjectV2(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException { + // V2 key pairs are DSA. + return DSACryptoImplementation.fromJSONObject(o); + } + public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { Long version = o.getLong("version"); - if (version == null || version.intValue() != 1) { - throw new IllegalStateException("version must be 1"); + if (version == null) { + throw new IllegalStateException("version must not be null"); } + + final int v = version.intValue(); + if (v == 2) { + // The most common case is the most recent version. + return fromJSONObjectV2(stateLabel, o); + } + if (v == 1) { + final State state = fromJSONObjectV1(stateLabel, o); + return migrateV1toV2(stateLabel, state); + } + throw new IllegalStateException("version must be in {1, 2}"); + } + + protected static State fromJSONObjectV1(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { switch (stateLabel) { case Engaged: return new Engaged( @@ -35,7 +82,7 @@ public class StateFactory { Utils.hex2Byte(o.getString("sessionToken")), Utils.hex2Byte(o.getString("kA")), Utils.hex2Byte(o.getString("kB")), - RSACryptoImplementation.fromJSONObject(o.getObject("keyPair"))); + keyPairFromJSONObjectV1(o.getObject("keyPair"))); case Married: return new Married( o.getString("email"), @@ -43,7 +90,7 @@ public class StateFactory { Utils.hex2Byte(o.getString("sessionToken")), Utils.hex2Byte(o.getString("kA")), Utils.hex2Byte(o.getString("kB")), - RSACryptoImplementation.fromJSONObject(o.getObject("keyPair")), + keyPairFromJSONObjectV1(o.getObject("keyPair")), o.getString("certificate")); case Separated: return new Separated( @@ -59,4 +106,81 @@ public class StateFactory { throw new IllegalStateException("unrecognized state label: " + stateLabel); } } + + /** + * Exactly the same as {@link fromJSONObjectV1}, except that all key pairs are DSA key pairs. + */ + protected static State fromJSONObjectV2(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException { + switch (stateLabel) { + case Cohabiting: + return new Cohabiting( + o.getString("email"), + o.getString("uid"), + Utils.hex2Byte(o.getString("sessionToken")), + Utils.hex2Byte(o.getString("kA")), + Utils.hex2Byte(o.getString("kB")), + keyPairFromJSONObjectV2(o.getObject("keyPair"))); + case Married: + return new Married( + o.getString("email"), + o.getString("uid"), + Utils.hex2Byte(o.getString("sessionToken")), + Utils.hex2Byte(o.getString("kA")), + Utils.hex2Byte(o.getString("kB")), + keyPairFromJSONObjectV2(o.getObject("keyPair")), + o.getString("certificate")); + default: + return fromJSONObjectV1(stateLabel, o); + } + } + + protected static void logMigration(State from, State to) { + if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) { + return; + } + try { + FxAccountConstants.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString()); + } catch (Exception e) { + Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e); + } + FxAccountConstants.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString()); + } + + protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException { + if (state == null) { + // This should never happen, but let's be careful. + Logger.error(LOG_TAG, "Got null state in migrateV1toV2; returning null."); + return state; + } + + Logger.info(LOG_TAG, "Migrating V1 persisted State to V2; stateLabel: " + stateLabel); + + // In V1, we use an RSA keyPair. In V2, we use a DSA keyPair. Only + // Cohabiting and Married states have a persisted keyPair at all; all + // other states need no conversion at all. + switch (stateLabel) { + case Cohabiting: { + // In the Cohabiting state, we can just generate a new key pair and move on. + final Cohabiting cohabiting = (Cohabiting) state; + final BrowserIDKeyPair keyPair = generateKeyPair(); + final State migrated = new Cohabiting(cohabiting.email, cohabiting.uid, cohabiting.sessionToken, cohabiting.kA, cohabiting.kB, keyPair); + logMigration(cohabiting, migrated); + return migrated; + } + case Married: { + // In the Married state, we cannot only change the key pair: the stored + // certificate signs the public key of the now obsolete key pair. We + // regress to the Cohabiting state; the next time we sync, we should + // advance back to Married. + final Married married = (Married) state; + final BrowserIDKeyPair keyPair = generateKeyPair(); + final State migrated = new Cohabiting(married.email, married.uid, married.sessionToken, married.kA, married.kB, keyPair); + logMigration(married, migrated); + return migrated; + } + default: + // Otherwise, V1 and V2 states are identical. + return state; + } + } } diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java index dbc7b0482ee..4059b3b396c 100644 --- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -20,7 +20,6 @@ import org.mozilla.gecko.background.fxa.FxAccountClient20; import org.mozilla.gecko.background.fxa.SkewHandler; import org.mozilla.gecko.browserid.BrowserIDKeyPair; import org.mozilla.gecko.browserid.JSONWebTokenUtils; -import org.mozilla.gecko.browserid.RSACryptoImplementation; import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient; import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate; import org.mozilla.gecko.fxa.FirefoxAccounts; @@ -34,6 +33,7 @@ import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition; import org.mozilla.gecko.fxa.login.Married; import org.mozilla.gecko.fxa.login.State; import org.mozilla.gecko.fxa.login.State.StateLabel; +import org.mozilla.gecko.fxa.login.StateFactory; import org.mozilla.gecko.sync.BackoffHandler; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.GlobalSession; @@ -537,7 +537,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { @Override public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException { - return RSACryptoImplementation.generateKeyPair(1024); + return StateFactory.generateKeyPair(); } @Override diff --git a/mobile/android/base/gfx/LayerView.java b/mobile/android/base/gfx/LayerView.java index b71bec03686..a33c3c1c88a 100644 --- a/mobile/android/base/gfx/LayerView.java +++ b/mobile/android/base/gfx/LayerView.java @@ -23,7 +23,6 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; @@ -325,7 +324,6 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener SurfaceHolder holder = mSurfaceView.getHolder(); holder.addCallback(new SurfaceListener()); - holder.setFormat(PixelFormat.RGB_565); } } diff --git a/mobile/android/base/home/HomeFragment.java b/mobile/android/base/home/HomeFragment.java index 34eb43ec393..40428e203a8 100644 --- a/mobile/android/base/home/HomeFragment.java +++ b/mobile/android/base/home/HomeFragment.java @@ -22,6 +22,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; +import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.util.Log; @@ -86,6 +87,11 @@ abstract class HomeFragment extends Fragment { menu.setHeaderTitle(info.getDisplayTitle()); + // Hide ununsed menu items. + menu.findItem(R.id.top_sites_edit).setVisible(false); + menu.findItem(R.id.top_sites_pin).setVisible(false); + menu.findItem(R.id.top_sites_unpin).setVisible(false); + // Hide the "Edit" menuitem if this item isn't a bookmark, // or if this is a reading list item. if (!info.hasBookmarkId() || info.isInReadingList()) { @@ -152,7 +158,10 @@ abstract class HomeFragment extends Fragment { flags |= Tabs.LOADURL_PRIVATE; final String url = (info.isInReadingList() ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url); - Tabs.getInstance().loadUrl(url, flags); + + // Some pinned site items have "user-entered" urls. URLs entered in the PinSiteDialog are wrapped in + // a special URI until we can get a valid URL. If the url is a user-entered url, decode the URL before loading it. + Tabs.getInstance().loadUrl(decodeUserEnteredUrl(url), flags); Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show(); return true; } @@ -218,6 +227,23 @@ abstract class HomeFragment extends Fragment { return mCanLoadHint; } + /** + * Given a url with a user-entered scheme, extract the + * scheme-specific component. For e.g, given "user-entered://www.google.com", + * this method returns "//www.google.com". If the passed url + * does not have a user-entered scheme, the same url will be returned. + * + * @param url to be decoded + * @return url component entered by user + */ + public static String decodeUserEnteredUrl(String url) { + Uri uri = Uri.parse(url); + if ("user-entered".equals(uri.getScheme())) { + return uri.getSchemeSpecificPart(); + } + return url; + } + protected abstract void load(); protected boolean canLoad() { diff --git a/mobile/android/base/home/ReadingListPanel.java b/mobile/android/base/home/ReadingListPanel.java index df1b6e0cb3a..1059b0ad8e4 100644 --- a/mobile/android/base/home/ReadingListPanel.java +++ b/mobile/android/base/home/ReadingListPanel.java @@ -215,13 +215,13 @@ public class ReadingListPanel extends HomeFragment { @Override public void bindView(View view, Context context, Cursor cursor) { - final TwoLinePageRow row = (TwoLinePageRow) view; + final ReadingListRow row = (ReadingListRow) view; row.updateFromCursor(cursor); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - return LayoutInflater.from(parent.getContext()).inflate(R.layout.bookmark_item_row, parent, false); + return LayoutInflater.from(parent.getContext()).inflate(R.layout.reading_list_item_row, parent, false); } } diff --git a/mobile/android/base/home/ReadingListRow.java b/mobile/android/base/home/ReadingListRow.java new file mode 100644 index 00000000000..49a1f2532fc --- /dev/null +++ b/mobile/android/base/home/ReadingListRow.java @@ -0,0 +1,43 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.home; + +import org.mozilla.gecko.R; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.AboutPages; +import org.mozilla.gecko.home.TwoLinePageRow; + +import android.content.Context; +import android.util.AttributeSet; + +public class ReadingListRow extends TwoLinePageRow { + + public ReadingListRow(Context context) { + this(context, null); + } + + public ReadingListRow(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void updateDisplayedUrl() { + String pageUrl = getUrl(); + + boolean isPrivate = Tabs.getInstance().getSelectedTab().isPrivate(); + Tab tab = Tabs.getInstance().getFirstTabForUrl(pageUrl, isPrivate); + + if (tab != null && AboutPages.isAboutReader(tab.getURL())) { + setUrl(R.string.switch_to_tab); + setSwitchToTabIcon(R.drawable.ic_url_bar_tab); + } else { + setUrl(pageUrl); + setSwitchToTabIcon(NO_ICON); + } + } + +} diff --git a/mobile/android/base/home/TopSitesGridView.java b/mobile/android/base/home/TopSitesGridView.java index d6338cbba35..72141509843 100644 --- a/mobile/android/base/home/TopSitesGridView.java +++ b/mobile/android/base/home/TopSitesGridView.java @@ -12,7 +12,6 @@ import org.mozilla.gecko.ThumbnailHelper; import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper; import org.mozilla.gecko.db.BrowserDB.URLColumns; import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; -import org.mozilla.gecko.util.StringUtils; import android.content.Context; import android.content.res.TypedArray; @@ -105,7 +104,7 @@ public class TopSitesGridView extends GridView { TopSitesGridItemView row = (TopSitesGridItemView) view; // Decode "user-entered" URLs before loading them. - String url = TopSitesPanel.decodeUserEnteredUrl(row.getUrl()); + String url = HomeFragment.decodeUserEnteredUrl(row.getUrl()); // If the url is empty, the user can pin a site. // If not, navigate to the page given by the url. @@ -125,7 +124,14 @@ public class TopSitesGridView extends GridView { @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { Cursor cursor = (Cursor) parent.getItemAtPosition(position); - mContextMenuInfo = new TopSitesGridContextMenuInfo(view, position, id, cursor); + + if (cursor == null) { + mContextMenuInfo = null; + return false; + } + + mContextMenuInfo = new TopSitesGridContextMenuInfo(view, position, id); + updateContextMenuFromCursor(mContextMenuInfo, cursor); return showContextMenuForChild(TopSitesGridView.this); } }); @@ -221,6 +227,18 @@ public class TopSitesGridView extends GridView { return mContextMenuInfo; } + /* + * Update the fields of a TopSitesGridContextMenuInfo object + * from a cursor. + * + * @param info context menu info object to be updated + * @param cursor used to update the context menu info object + */ + private void updateContextMenuFromCursor(TopSitesGridContextMenuInfo info, Cursor cursor) { + info.url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); + info.title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); + info.isPinned = ((TopSitesCursorWrapper) cursor).isPinned(); + } /** * Set an url open listener to be used by this view. * @@ -240,29 +258,13 @@ public class TopSitesGridView extends GridView { } /** - * A ContextMenuInfo for TopBoomarksView that adds details from the cursor. + * Stores information regarding the creation of the context menu for a GridView item. */ - public static class TopSitesGridContextMenuInfo extends AdapterContextMenuInfo { + public static class TopSitesGridContextMenuInfo extends HomeContextMenuInfo { + public boolean isPinned = false; - public String url; - public String title; - public boolean isPinned; - - public TopSitesGridContextMenuInfo(View targetView, int position, long id, Cursor cursor) { + public TopSitesGridContextMenuInfo(View targetView, int position, long id) { super(targetView, position, id); - - if (cursor == null) { - return; - } - - url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); - title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); - isPinned = ((TopSitesCursorWrapper) cursor).isPinned(); - } - - public String getDisplayTitle() { - return TextUtils.isEmpty(title) ? - StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS)) : title; } } } diff --git a/mobile/android/base/home/TopSitesPanel.java b/mobile/android/base/home/TopSitesPanel.java index a6e084e7c9f..bbc24f4cfe4 100644 --- a/mobile/android/base/home/TopSitesPanel.java +++ b/mobile/android/base/home/TopSitesPanel.java @@ -10,9 +10,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.Map; -import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.R; -import org.mozilla.gecko.Tabs; import org.mozilla.gecko.db.BrowserContract.Combined; import org.mozilla.gecko.db.BrowserContract.Thumbnails; import org.mozilla.gecko.db.BrowserDB; @@ -30,7 +28,6 @@ import org.mozilla.gecko.util.ThreadUtils; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; @@ -52,7 +49,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; -import android.widget.Toast; /** * Fragment that displays frecency search results in a ListView. @@ -267,17 +263,21 @@ public class TopSitesPanel extends HomeFragment { return; } - // HomeFragment will handle the default case. - if (menuInfo instanceof HomeContextMenuInfo) { - super.onCreateContextMenu(menu, view, menuInfo); - } - if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) { + // Long pressed item was not a Top Sites GridView item. Superclass + // can handle this. + super.onCreateContextMenu(menu, view, menuInfo); return; } + // Long pressed item was a Top Sites GridView item, handle it. MenuInflater inflater = new MenuInflater(view.getContext()); - inflater.inflate(R.menu.top_sites_contextmenu, menu); + inflater.inflate(R.menu.home_contextmenu, menu); + + // Hide ununsed menu items. + menu.findItem(R.id.home_open_in_reader).setVisible(false); + menu.findItem(R.id.home_edit_bookmark).setVisible(false); + menu.findItem(R.id.home_remove).setVisible(false); TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo; menu.setHeaderTitle(info.getDisplayTitle()); @@ -289,8 +289,8 @@ public class TopSitesPanel extends HomeFragment { menu.findItem(R.id.top_sites_unpin).setVisible(false); } } else { - menu.findItem(R.id.top_sites_open_new_tab).setVisible(false); - menu.findItem(R.id.top_sites_open_private_tab).setVisible(false); + menu.findItem(R.id.home_open_new_tab).setVisible(false); + menu.findItem(R.id.home_open_private_tab).setVisible(false); menu.findItem(R.id.top_sites_pin).setVisible(false); menu.findItem(R.id.top_sites_unpin).setVisible(false); } @@ -298,9 +298,13 @@ public class TopSitesPanel extends HomeFragment { @Override public boolean onContextItemSelected(MenuItem item) { + if (super.onContextItemSelected(item)) { + // HomeFragment was able to handle to selected item. + return true; + } + ContextMenuInfo menuInfo = item.getMenuInfo(); - // HomeFragment will handle the default case. if (menuInfo == null || !(menuInfo instanceof TopSitesGridContextMenuInfo)) { return false; } @@ -309,21 +313,6 @@ public class TopSitesPanel extends HomeFragment { final Activity activity = getActivity(); final int itemId = item.getItemId(); - if (itemId == R.id.top_sites_open_new_tab || itemId == R.id.top_sites_open_private_tab) { - if (info.url == null) { - Log.e(LOGTAG, "Can't open in new tab because URL is null"); - return false; - } - - int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND; - if (item.getItemId() == R.id.top_sites_open_private_tab) - flags |= Tabs.LOADURL_PRIVATE; - - // Decode "user-entered" URLs before loading them. - Tabs.getInstance().loadUrl(decodeUserEnteredUrl(info.url), flags); - Toast.makeText(activity, R.string.new_tab_opened, Toast.LENGTH_SHORT).show(); - return true; - } if (itemId == R.id.top_sites_pin) { final String url = info.url; @@ -361,28 +350,6 @@ public class TopSitesPanel extends HomeFragment { return true; } - if (itemId == R.id.home_share) { - if (info.url == null) { - Log.w(LOGTAG, "Share not enabled for context menu because URL is null."); - return false; - } else { - GeckoAppShell.openUriExternal(info.url, SHARE_MIME_TYPE, "", "", - Intent.ACTION_SEND, info.getDisplayTitle()); - return true; - } - } - - if (itemId == R.id.home_add_to_launcher) { - if (info.url == null) { - Log.w(LOGTAG, "Not enabling 'Add to home page' because URL is null."); - return false; - } - - // Fetch an icon big enough for use as a home screen icon. - Favicons.getPreferredSizeFaviconForPage(info.url, new GeckoAppShell.CreateShortcutFaviconLoadedListener(info.url, info.getDisplayTitle())); - return true; - } - return false; } @@ -403,14 +370,6 @@ public class TopSitesPanel extends HomeFragment { return Uri.fromParts("user-entered", url, null).toString(); } - static String decodeUserEnteredUrl(String url) { - Uri uri = Uri.parse(url); - if ("user-entered".equals(uri.getScheme())) { - return uri.getSchemeSpecificPart(); - } - return url; - } - /** * Listener for editing pinned sites. */ diff --git a/mobile/android/base/home/TwoLinePageRow.java b/mobile/android/base/home/TwoLinePageRow.java index ea32c093a46..f9a5dce12bc 100644 --- a/mobile/android/base/home/TwoLinePageRow.java +++ b/mobile/android/base/home/TwoLinePageRow.java @@ -30,7 +30,7 @@ import android.widget.TextView; public class TwoLinePageRow extends LinearLayout implements Tabs.OnTabsChangedListener { - private static final int NO_ICON = 0; + protected static final int NO_ICON = 0; private final TextView mTitle; private final TextView mUrl; @@ -122,15 +122,19 @@ public class TwoLinePageRow extends LinearLayout mTitle.setText(text); } - private void setUrl(String text) { + protected void setUrl(String text) { mUrl.setText(text); } - private void setUrl(int stringId) { + protected void setUrl(int stringId) { mUrl.setText(stringId); } - private void setSwitchToTabIcon(int iconId) { + protected String getUrl() { + return mPageUrl; + } + + protected void setSwitchToTabIcon(int iconId) { if (mSwitchToTabIconId == iconId) { return; } @@ -159,10 +163,10 @@ public class TwoLinePageRow extends LinearLayout /** * Replaces the page URL with "Switch to tab" if there is already a tab open with that URL. - * Only looks for tabs that are either private or non-private, depending on the current + * Only looks for tabs that are either private or non-private, depending on the current * selected tab. */ - private void updateDisplayedUrl() { + protected void updateDisplayedUrl() { boolean isPrivate = Tabs.getInstance().getSelectedTab().isPrivate(); Tab tab = Tabs.getInstance().getFirstTabForUrl(mPageUrl, isPrivate); if (!mShowIcons || tab == null) { diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index e028359017d..de3c69d0588 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -268,6 +268,7 @@ gbjar.sources += [ 'home/PanelViewItemHandler.java', 'home/PinSiteDialog.java', 'home/ReadingListPanel.java', + 'home/ReadingListRow.java', 'home/SearchEngine.java', 'home/SearchEngineRow.java', 'home/SearchLoader.java', diff --git a/mobile/android/base/resources/layout/reading_list_item_row.xml b/mobile/android/base/resources/layout/reading_list_item_row.xml new file mode 100644 index 00000000000..22b563a01b3 --- /dev/null +++ b/mobile/android/base/resources/layout/reading_list_item_row.xml @@ -0,0 +1,10 @@ + + + + diff --git a/mobile/android/base/resources/menu/home_contextmenu.xml b/mobile/android/base/resources/menu/home_contextmenu.xml index 3146a7df37c..972f3ad5171 100644 --- a/mobile/android/base/resources/menu/home_contextmenu.xml +++ b/mobile/android/base/resources/menu/home_contextmenu.xml @@ -17,6 +17,15 @@ + + + + + + diff --git a/mobile/android/base/resources/menu/top_sites_contextmenu.xml b/mobile/android/base/resources/menu/top_sites_contextmenu.xml deleted file mode 100644 index 336c1f82a9e..00000000000 --- a/mobile/android/base/resources/menu/top_sites_contextmenu.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - -

- - - - - - - - - - - - - - - diff --git a/services/common/hawkclient.js b/services/common/hawkclient.js index 6a77bb61ec2..11a96af1011 100644 --- a/services/common/hawkclient.js +++ b/services/common/hawkclient.js @@ -32,6 +32,7 @@ Cu.import("resource://gre/modules/FxAccountsCommon.js"); Cu.import("resource://services-common/utils.js"); Cu.import("resource://services-crypto/utils.js"); Cu.import("resource://services-common/hawkrequest.js"); +Cu.import("resource://services-common/observers.js"); Cu.import("resource://gre/modules/Promise.jsm"); /* @@ -75,6 +76,10 @@ this.HawkClient.prototype = { retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter; if (retryAfter) { errorObj.retryAfter = retryAfter; + // and notify observers of the retry interval + if (this.observerPrefix) { + Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter); + } } return errorObj; }, @@ -154,6 +159,11 @@ this.HawkClient.prototype = { " - Status text: " + restResponse.statusText, " - Response text: " + restResponse.body); + // All responses may have backoff headers, which are a server-side safety + // valve to allow slowing down clients without hurting performance. + self._maybeNotifyBackoff(restResponse, "x-weave-backoff"); + self._maybeNotifyBackoff(restResponse, "x-backoff"); + if (error) { // When things really blow up, reconstruct an error object that follows // the general format of the server on error responses. @@ -162,7 +172,7 @@ this.HawkClient.prototype = { self._updateClockOffset(restResponse.headers["date"]); - if (status === 401 && retryOK) { + if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) { // Retry once if we were rejected due to a bad timestamp. // Clock offset is adjusted already in the top of this function. log.debug("Received 401 for " + path + ": retrying"); @@ -209,6 +219,35 @@ this.HawkClient.prototype = { return deferred.promise; }, + /* + * The prefix used for all notifications sent by this module. This + * allows the handler of notifications to be sure they are handling + * notifications for the service they expect. + * + * If not set, no notifications will be sent. + */ + observerPrefix: null, + + // Given an optional header value, notify that a backoff has been requested. + _maybeNotifyBackoff: function (response, headerName) { + if (!this.observerPrefix) { + return; + } + let headerVal = response.headers[headerName]; + if (!headerVal) { + return; + } + let backoffInterval; + try { + backoffInterval = parseInt(headerVal, 10); + } catch (ex) { + this._log.error("hawkclient response had invalid backoff value in '" + + headerName + "' header: " + headerVal); + return; + } + Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval); + }, + // override points for testing. newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) { return new HAWKAuthenticatedRESTRequest(uri, credentials, extra); diff --git a/services/common/tokenserverclient.js b/services/common/tokenserverclient.js index 01945516c71..6664c6670ce 100644 --- a/services/common/tokenserverclient.js +++ b/services/common/tokenserverclient.js @@ -329,7 +329,8 @@ TokenServerClient.prototype = { return; } - // Any response status can have an X-Backoff header. + // Any response status can have X-Backoff or X-Weave-Backoff headers. + this._maybeNotifyBackoff(response, "x-weave-backoff"); this._maybeNotifyBackoff(response, "x-backoff"); // The service shouldn't have any 3xx, so we don't need to handle those. @@ -413,8 +414,20 @@ TokenServerClient.prototype = { }); }, + /* + * The prefix used for all notifications sent by this module. This + * allows the handler of notifications to be sure they are handling + * notifications for the service they expect. + * + * If not set, no notifications will be sent. + */ + observerPrefix: null, + // Given an optional header value, notify that a backoff has been requested. _maybeNotifyBackoff: function (response, headerName) { + if (!this.observerPrefix) { + return; + } let headerVal = response.headers[headerName]; if (!headerVal) { return; @@ -427,7 +440,7 @@ TokenServerClient.prototype = { headerName + "' header: " + headerVal); return; } - Observers.notify("tokenserver:backoff:interval", backoffInterval); + Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval); }, // override points for testing. diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm index bfaee3177db..f74fdb12c8c 100644 --- a/services/fxaccounts/FxAccounts.jsm +++ b/services/fxaccounts/FxAccounts.jsm @@ -460,9 +460,26 @@ FxAccountsInternal.prototype = { }, signOut: function signOut() { - this.abortExistingFlow(); - this.currentAccountState.signedInUser = null; // clear in-memory cache - return this.signedInUserStorage.set(null).then(() => { + let currentState = this.currentAccountState; + let fxAccountsClient = this.fxAccountsClient; + let sessionToken; + return currentState.getUserAccountData().then(data => { + // Save the session token for use in the call to signOut below. + sessionToken = data && data.sessionToken; + this.abortExistingFlow(); + this.currentAccountState.signedInUser = null; // clear in-memory cache + return this.signedInUserStorage.set(null); + }).then(() => { + // Wrap this in a promise so *any* errors in signOut won't + // block the local sign out. This is *not* returned. + Promise.resolve().then(() => { + // This can happen in the background and shouldn't block + // the user from signing out. The server must tolerate + // clients just disappearing, so this call should be best effort. + return fxAccountsClient.signOut(sessionToken); + }).then(null, err => { + log.error("Error during remote sign out of Firefox Accounts: " + err); + }); this.notifyObservers(ONLOGOUT_NOTIFICATION); }); }, @@ -497,15 +514,18 @@ FxAccountsInternal.prototype = { } if (!currentState.whenKeysReadyDeferred) { currentState.whenKeysReadyDeferred = Promise.defer(); - this.fetchAndUnwrapKeys(data.keyFetchToken).then(data => { - if (!data.kA || !data.kB) { - currentState.whenKeysReadyDeferred.reject( - new Error("user data missing kA or kB") - ); - return; - } - currentState.whenKeysReadyDeferred.resolve(data); - }); + this.fetchAndUnwrapKeys(data.keyFetchToken).then( + data => { + if (!data.kA || !data.kB) { + currentState.whenKeysReadyDeferred.reject( + new Error("user data missing kA or kB") + ); + return; + } + currentState.whenKeysReadyDeferred.resolve(data); + }, + err => currentState.whenKeysReadyDeferred.reject(err) + ); } return currentState.whenKeysReadyDeferred.promise; }).then(result => currentState.resolve(result)); diff --git a/services/fxaccounts/FxAccountsClient.jsm b/services/fxaccounts/FxAccountsClient.jsm index fe3fdde9f0b..ba96041a679 100644 --- a/services/fxaccounts/FxAccountsClient.jsm +++ b/services/fxaccounts/FxAccountsClient.jsm @@ -23,6 +23,7 @@ this.FxAccountsClient = function(host = HOST) { // The FxA auth server expects requests to certain endpoints to be authorized // using Hawk. this.hawk = new HawkClient(host); + this.hawk.observerPrefix = "FxA:hawk"; // Manage server backoff state. C.f. // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js index ff3507abf6b..1c78239a930 100644 --- a/services/fxaccounts/tests/xpcshell/test_accounts.js +++ b/services/fxaccounts/tests/xpcshell/test_accounts.js @@ -77,6 +77,8 @@ function MockFxAccountsClient() { this.signCertificate = function() { throw "no" }; + this.signOut = function() { return Promise.resolve(); }; + FxAccountsClient.apply(this); } MockFxAccountsClient.prototype = { @@ -537,6 +539,45 @@ add_test(function test_resend_email() { }); }); +add_test(function test_sign_out() { + do_test_pending(); + let fxa = new MockFxAccounts(); + let remoteSignOutCalled = false; + let client = fxa.internal.fxAccountsClient; + client.signOut = function() { remoteSignOutCalled = true; return Promise.resolve(); }; + makeObserver(ONLOGOUT_NOTIFICATION, function() { + log.debug("test_sign_out_with_remote_error observed onlogout"); + // user should be undefined after sign out + fxa.internal.getUserAccountData().then(user => { + do_check_eq(user, null); + do_check_true(remoteSignOutCalled); + do_test_finished(); + run_next_test(); + }); + }); + fxa.signOut(); +}); + +add_test(function test_sign_out_with_remote_error() { + do_test_pending(); + let fxa = new MockFxAccounts(); + let client = fxa.internal.fxAccountsClient; + let remoteSignOutCalled = false; + // Force remote sign out to trigger an error + client.signOut = function() { remoteSignOutCalled = true; throw "Remote sign out error"; }; + makeObserver(ONLOGOUT_NOTIFICATION, function() { + log.debug("test_sign_out_with_remote_error observed onlogout"); + // user should be undefined after sign out + fxa.internal.getUserAccountData().then(user => { + do_check_eq(user, null); + do_check_true(remoteSignOutCalled); + do_test_finished(); + run_next_test(); + }); + }); + fxa.signOut(); +}); + /* * End of tests. * Utility functions follow. diff --git a/services/healthreport/docs/dataformat.rst b/services/healthreport/docs/dataformat.rst index ba539cec898..43458e9ec00 100644 --- a/services/healthreport/docs/dataformat.rst +++ b/services/healthreport/docs/dataformat.rst @@ -387,6 +387,21 @@ thisPingDate version Integer version of this payload format. Currently only 1 is defined. +clientID + An identifier that identifies the client that is submitting data. + + This property may not be present in older clients. + + See :ref:`healthreport_identifiers` for more info on identifiers. + +clientIDVersion + Integer version associated with the generation semantics for the + ``clientID``. + + If the value is ``1``, ``clientID`` is a randomly-generated UUID. + + This property may not be present in older clients. + data Object holding data constituting health report. diff --git a/services/healthreport/docs/identifiers.rst b/services/healthreport/docs/identifiers.rst new file mode 100644 index 00000000000..82ad0e49e69 --- /dev/null +++ b/services/healthreport/docs/identifiers.rst @@ -0,0 +1,83 @@ +.. _healthreport_identifiers: + +=========== +Identifiers +=========== + +Firefox Health Report records some identifiers to keep track of clients +and uploaded documents. + +Identifier Types +================ + +Document/Upload IDs +------------------- + +A random UUID called the *Document ID* or *Upload ID* is generated when the FHR +client creates or uploads a new document. + +When clients generate a new *Document ID*, they persist this ID to disk +**before** the upload attempt. + +As part of the upload, the client sends all old *Document IDs* to the server +and asks the server to delete them. In well-behaving clients, the server +has a single record for each client with a randomly-changing *Document ID*. + +Client IDs +---------- + +A *Client ID* is an identifier that **attempts** to uniquely identify an +individual FHR client. Please note the emphasis on *attempts* in that last +sentence: *Client IDs* do not guarantee uniqueness. + +The *Client ID* is generated when the client first runs or as needed. + +The *Client ID* is transferred to the server as part of every upload. The +server is thus able to affiliate multiple document uploads with a single +*Client ID*. + +Client ID Versions +^^^^^^^^^^^^^^^^^^ + +The semantics for how a *Client ID* is generated are versioned. + +Version 1 + The *Client ID* is a randomly-generated UUID. + +History of Identifiers +====================== + +In the beginning, there were just *Document IDs*. The thinking was clients +would clean up after themselves and leave at most 1 active document on the +server. + +Unfortunately, this did not work out. Using brute force analysis to +deduplicate records on the server, a number of interesting patterns emerged. + +Orphaning + Clients would upload a new payload while not deleting the old payload. + +Divergent records + Records would share data up to a certain date and then the data would + almost completely diverge. This appears to be indicative of profile + copying. + +Rollback + Records would share data up to a certain date. Each record in this set + would contain data for a day or two but no extra data. This could be + explained by filesystem rollback on the client. + +A significant percentage of the records on the server belonged to +misbehaving clients. Identifying these records was extremely resource +intensive and error-prone. These records were undermining the ability +to use Firefox Health Report data. + +Thus, the *Client ID* was born. The intent of the *Client ID* was to +uniquely identify clients so the extreme effort required and the +questionable reliability of deduplicating server data would become +problems of the past. + +The *Client ID* was originally a randomly-generated UUID (version 1). This +allowed detection of orphaning and rollback. However, these version 1 +*Client IDs* were still susceptible to use on multiple profiles and +machines if the profile was copied. diff --git a/services/healthreport/docs/index.rst b/services/healthreport/docs/index.rst index 8668fb7a557..047863bb5a6 100644 --- a/services/healthreport/docs/index.rst +++ b/services/healthreport/docs/index.rst @@ -24,6 +24,7 @@ are very specific to what the Firefox Health Report feature requires. architecture dataformat + identifiers Legal and Privacy Concerns ========================== diff --git a/services/healthreport/healthreporter.jsm b/services/healthreport/healthreporter.jsm index 4065c3d34df..5666c2c2af7 100644 --- a/services/healthreport/healthreporter.jsm +++ b/services/healthreport/healthreporter.jsm @@ -60,11 +60,19 @@ const TELEMETRY_COLLECT_CHECKPOINT = "HEALTHREPORT_POST_COLLECT_CHECKPOINT_MS"; * * Instances are not meant to be created outside of a HealthReporter instance. * - * Please note that remote IDs are treated as a queue. When a new remote ID is - * added, it goes at the back of the queue. When we look for the current ID, we - * pop the ID at the front of the queue. This helps ensure that all IDs on the - * server are eventually deleted. This should eventually become irrelevant once - * the server supports multiple ID deletion. + * There are two types of IDs associated with clients. + * + * Since the beginning of FHR, there has existed a per-upload ID: a UUID is + * generated at upload time and associated with the state before upload starts. + * That same upload includes a request to delete all other upload IDs known by + * the client. + * + * Per-upload IDs had the unintended side-effect of creating "orphaned" + * records/upload IDs on the server. So, a stable client identifer has been + * introduced. This client identifier is generated when it's missing and sent + * as part of every upload. + * + * There is a high chance we may remove upload IDs in the future. */ function HealthReporterState(reporter) { this._reporter = reporter; @@ -89,6 +97,20 @@ function HealthReporterState(reporter) { } HealthReporterState.prototype = Object.freeze({ + /** + * Persistent string identifier associated with this client. + */ + get clientID() { + return this._s.clientID; + }, + + /** + * The version associated with the client ID. + */ + get clientIDVersion() { + return this._s.clientIDVersion; + }, + get lastPingDate() { return new Date(this._s.lastPingTime); }, @@ -117,9 +139,19 @@ HealthReporterState.prototype = Object.freeze({ let resetObjectState = function () { this._s = { + // The payload version. This is bumped whenever there is a + // backwards-incompatible change. v: 1, + // The persistent client identifier. + clientID: CommonUtils.generateUUID(), + // Denotes the mechanism used to generate the client identifier. + // 1: Random UUID. + clientIDVersion: 1, + // Upload IDs that might be on the server. remoteIDs: [], + // When we last performed an uploaded. lastPingTime: 0, + // Tracks whether we removed an outdated payload. removedOutdatedLastpayload: false, }; }.bind(this); @@ -154,6 +186,23 @@ HealthReporterState.prototype = Object.freeze({ // comes along and fixes us. } + let regen = false; + if (!this._s.clientID) { + this._log.warn("No client ID stored. Generating random ID."); + regen = true; + } + + if (typeof(this._s.clientID) != "string") { + this._log.warn("Client ID is not a string. Regenerating."); + regen = true; + } + + if (regen) { + this._s.clientID = CommonUtils.generateUUID(); + this._s.clientIDVersion = 1; + yield this.save(); + } + // Always look for preferences. This ensures that downgrades followed // by reupgrades don't result in excessive data loss. for (let promise of this._migratePrefs()) { @@ -214,6 +263,24 @@ HealthReporterState.prototype = Object.freeze({ return this.removeRemoteIDs(ids); }, + /** + * Reset the client ID to something else. + * + * This fails if remote IDs are stored because changing the client ID + * while there is remote data will create orphaned records. + */ + resetClientID: function () { + if (this.remoteIDs.length) { + throw new Error("Cannot reset client ID while remote IDs are stored."); + } + + this._log.warn("Resetting client ID."); + this._s.clientID = CommonUtils.generateUUID(); + this._s.clientIDVersion = 1; + + return this.save(); + }, + _migratePrefs: function () { let prefs = this._reporter._prefs; @@ -897,6 +964,8 @@ AbstractHealthReporter.prototype = Object.freeze({ let o = { version: 2, + clientID: this._state.clientID, + clientIDVersion: this._state.clientIDVersion, thisPingDate: pingDateString, geckoAppInfo: this.obtainAppInfo(this._log), data: {last: {}, days: {}}, @@ -1457,9 +1526,23 @@ this.HealthReporter.prototype = Object.freeze({ this._log.warn("Deleting remote data."); let client = new BagheeraClient(this.serverURI); - return client.deleteDocument(this.serverNamespace, this.lastSubmitID) - .then(this._onBagheeraResult.bind(this, request, true, this._now()), - this._onSubmitDataRequestFailure.bind(this)); + return Task.spawn(function* doDelete() { + try { + let result = yield client.deleteDocument(this.serverNamespace, + this.lastSubmitID); + yield this._onBagheeraResult(request, true, this._now(), result); + } catch (ex) { + this._log.error("Error processing request to delete data: " + + CommonUtils.exceptionStr(error)); + } finally { + // If we don't have any remote documents left, nuke the ID. + // This is done for privacy reasons. Why preserve the ID if we + // don't need to? + if (!this.haveRemoteData()) { + yield this._state.resetClientID(); + } + } + }.bind(this)); }, }); diff --git a/services/healthreport/tests/xpcshell/test_healthreporter.js b/services/healthreport/tests/xpcshell/test_healthreporter.js index 5a4d2f78d0d..ad7aca9b8a4 100644 --- a/services/healthreport/tests/xpcshell/test_healthreporter.js +++ b/services/healthreport/tests/xpcshell/test_healthreporter.js @@ -110,6 +110,8 @@ add_task(function test_constructor() { do_check_eq(typeof(reporter._state), "object"); do_check_eq(reporter._state.lastPingDate.getTime(), 0); do_check_eq(reporter._state.remoteIDs.length, 0); + do_check_eq(reporter._state.clientIDVersion, 1); + do_check_neq(reporter._state.clientID, null); let failed = false; try { @@ -293,6 +295,9 @@ add_task(function test_remove_old_lastpayload() { add_task(function test_json_payload_simple() { let reporter = yield getReporter("json_payload_simple"); + let clientID = reporter._state.clientID; + do_check_neq(clientID, null); + try { let now = new Date(); let payload = yield reporter.getJSONPayload(); @@ -301,6 +306,9 @@ add_task(function test_json_payload_simple() { do_check_eq(original.version, 2); do_check_eq(original.thisPingDate, reporter._formatDate(now)); + do_check_eq(original.clientID, clientID); + do_check_eq(original.clientIDVersion, reporter._state.clientIDVersion); + do_check_eq(original.clientIDVersion, 1); do_check_eq(Object.keys(original.data.last).length, 0); do_check_eq(Object.keys(original.data.days).length, 0); do_check_false("notInitialized" in original); @@ -310,6 +318,7 @@ add_task(function test_json_payload_simple() { original = JSON.parse(yield reporter.getJSONPayload()); do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate)); + do_check_eq(original.clientID, clientID); // This could fail if we cross UTC day boundaries at the exact instance the // test is executed. Let's tempt fate. @@ -636,6 +645,7 @@ add_task(function test_data_submission_success() { let d = reporter.lastPingDate; let id = reporter.lastSubmitID; + let clientID = reporter._state.clientID; reporter._shutdown(); @@ -643,6 +653,7 @@ add_task(function test_data_submission_success() { reporter = yield getReporter("data_submission_success"); do_check_eq(reporter.lastSubmitID, id); do_check_eq(reporter.lastPingDate.getTime(), d.getTime()); + do_check_eq(reporter._state.clientID, clientID); reporter._shutdown(); } finally { @@ -703,6 +714,9 @@ add_task(function test_request_remote_data_deletion() { do_check_neq(id, null); do_check_true(server.hasDocument(reporter.serverNamespace, id)); + let clientID = reporter._state.clientID; + do_check_neq(clientID, null); + defineNow(policy, policy._futureDate(10 * 1000)); let promise = reporter.requestDeleteRemoteData(); @@ -711,6 +725,16 @@ add_task(function test_request_remote_data_deletion() { do_check_null(reporter.lastSubmitID); do_check_false(reporter.haveRemoteData()); do_check_false(server.hasDocument(reporter.serverNamespace, id)); + + // Client ID should be updated. + do_check_neq(reporter._state.clientID, null); + do_check_neq(reporter._state.clientID, clientID); + do_check_eq(reporter._state.clientIDVersion, 1); + + // And it should be persisted to disk. + let o = yield CommonUtils.readJSON(reporter._state._filename); + do_check_eq(o.clientID, reporter._state.clientID); + do_check_eq(o.clientIDVersion, 1); } finally { reporter._shutdown(); yield shutdownServer(server); @@ -1096,3 +1120,78 @@ add_task(function test_state_downgrade_upgrade() { reporter._shutdown(); } }); + +// Missing client ID in state should be created on state load. +add_task(function* test_state_create_client_id() { + let reporter = getHealthReporter("state_create_client_id"); + + yield CommonUtils.writeJSON({ + v: 1, + remoteIDs: ["id1", "id2"], + lastPingTime: Date.now(), + removeOutdatedLastPayload: true, + }, reporter._state._filename); + + try { + yield reporter.init(); + + do_check_eq(reporter.lastSubmitID, "id1"); + do_check_neq(reporter._state.clientID, null); + do_check_eq(reporter._state.clientID.length, 36); + do_check_eq(reporter._state.clientIDVersion, 1); + + let clientID = reporter._state.clientID; + + // The client ID should be persisted as soon as it is created. + reporter._shutdown(); + + reporter = getHealthReporter("state_create_client_id"); + yield reporter.init(); + do_check_eq(reporter._state.clientID, clientID); + } finally { + reporter._shutdown(); + } +}); + +// Invalid stored client ID is reset automatically. +add_task(function* test_empty_client_id() { + let reporter = getHealthReporter("state_empty_client_id"); + + yield CommonUtils.writeJSON({ + v: 1, + clientID: "", + remoteIDs: ["id1", "id2"], + lastPingTime: Date.now(), + removeOutdatedLastPayload: true, + }, reporter._state._filename); + + try { + yield reporter.init(); + + do_check_neq(reporter._state.clientID, null); + do_check_eq(reporter._state.clientID.length, 36); + } finally { + reporter._shutdown(); + } +}); + +add_task(function* test_nonstring_client_id() { + let reporter = getHealthReporter("state_nonstring_client_id"); + + yield CommonUtils.writeJSON({ + v: 1, + clientID: 42, + remoteIDs: ["id1", "id2"], + lastPingTime: Date.now(), + remoteOutdatedLastPayload: true, + }, reporter._state._filename); + + try { + yield reporter.init(); + + do_check_neq(reporter._state.clientID, null); + do_check_eq(reporter._state.clientID.length, 36); + } finally { + reporter._shutdown(); + } +}); diff --git a/services/sync/modules-testing/fxa_utils.js b/services/sync/modules-testing/fxa_utils.js index b635f953aa8..d472357a9ff 100644 --- a/services/sync/modules-testing/fxa_utils.js +++ b/services/sync/modules-testing/fxa_utils.js @@ -53,6 +53,9 @@ this.initializeIdentityWithTokenServerResponse = function(response) { MockTSC.prototype.newRESTRequest = function(url) { return new MockRESTRequest(url); } + // Arrange for the same observerPrefix as browserid_identity uses. + MockTSC.prototype.observerPrefix = "weave:service"; + // tie it all together. Weave.Status.__authManager = Weave.Service.identity = new BrowserIDManager(); Weave.Service._clusterManager = Weave.Service.identity.createClusterManager(Weave.Service); diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js index 0489cd2b81a..f686eece41a 100644 --- a/services/sync/modules/browserid_identity.js +++ b/services/sync/modules/browserid_identity.js @@ -80,6 +80,7 @@ this.BrowserIDManager = function BrowserIDManager() { // the test suite. this._fxaService = fxAccounts; this._tokenServerClient = new TokenServerClient(); + this._tokenServerClient.observerPrefix = "weave:service"; // will be a promise that resolves when we are ready to authenticate this.whenReadyToAuthenticate = null; this._log = log; @@ -222,8 +223,9 @@ this.BrowserIDManager.prototype = { } } }).then(() => { - return this._fetchSyncKeyBundle(); - }).then(() => { + return this._fetchTokenForUser(); + }).then(token => { + this._token = token; this._shouldHaveSyncKeyBundle = true; // and we should actually have one... this.whenReadyToAuthenticate.resolve(); this._log.info("Background fetch for key bundle done"); @@ -436,20 +438,6 @@ this.BrowserIDManager.prototype = { return true; }, - _fetchSyncKeyBundle: function() { - // Fetch a sync token for the logged in user from the token server. - return this._fxaService.getKeys().then(userData => { - this._updateSignedInUser(userData); // throws if the user changed. - return this._fetchTokenForUser().then(token => { - this._token = token; - // both Jelly and FxAccounts give us kA/kB as hex. - let kB = Utils.hexToBytes(userData.kB); - this._syncKeyBundle = deriveKeyBundle(kB); - return; - }); - }); - }, - // Refresh the sync token for our user. _fetchTokenForUser: function() { let tokenServerURI = Svc.Prefs.get("tokenServerURI"); @@ -458,47 +446,49 @@ this.BrowserIDManager.prototype = { let fxa = this._fxaService; let userData = this._signedInUser; - // Both Jelly and FxAccounts give us kB as hex - let kBbytes = CommonUtils.hexToBytes(userData.kB); - let headers = {"X-Client-State": this._computeXClientState(kBbytes)}; log.info("Fetching assertion and token from: " + tokenServerURI); - function getToken(tokenServerURI, assertion) { + let maybeFetchKeys = () => { + // This is called at login time and every time we need a new token - in + // the latter case we already have kA and kB, so optimise that case. + if (userData.kA && userData.kB) { + return; + } + return this._fxaService.getKeys().then( + newUserData => { + userData = newUserData; + this._updateSignedInUser(userData); // throws if the user changed. + } + ); + } + + let getToken = (tokenServerURI, assertion) => { log.debug("Getting a token"); let deferred = Promise.defer(); let cb = function (err, token) { if (err) { - log.info("TokenServerClient.getTokenFromBrowserIDAssertion() failed with: " + err); - if (err.response && err.response.status === 401) { - err = new AuthenticationError(err); - } return deferred.reject(err); - } else { - log.debug("Successfully got a sync token"); - return deferred.resolve(token); } + log.debug("Successfully got a sync token"); + return deferred.resolve(token); }; + let kBbytes = CommonUtils.hexToBytes(userData.kB); + let headers = {"X-Client-State": this._computeXClientState(kBbytes)}; client.getTokenFromBrowserIDAssertion(tokenServerURI, assertion, cb, headers); return deferred.promise; } - function getAssertion() { + let getAssertion = () => { log.debug("Getting an assertion"); let audience = Services.io.newURI(tokenServerURI, null, null).prePath; - return fxa.getAssertion(audience).then(null, err => { - log.error("fxa.getAssertion() failed with: " + err.code + " - " + err.message); - if (err.code === 401) { - throw new AuthenticationError("Unable to get assertion for user"); - } else { - throw err; - } - }); + return fxa.getAssertion(audience); }; // wait until the account email is verified and we know that // getAssertion() will return a real assertion (not null). return fxa.whenVerified(this._signedInUser) + .then(() => maybeFetchKeys()) .then(() => getAssertion()) .then(assertion => getToken(tokenServerURI, assertion)) .then(token => { @@ -506,9 +496,23 @@ this.BrowserIDManager.prototype = { // before it actually expires. This is to avoid sync storage errors // otherwise, we get a nasty notification bar briefly. Bug 966568. token.expiration = this._now() + (token.duration * 1000) * 0.80; + if (!this._syncKeyBundle) { + // We are given kA/kB as hex. + this._syncKeyBundle = deriveKeyBundle(Utils.hexToBytes(userData.kB)); + } return token; }) .then(null, err => { + // TODO: unify these errors - we need to handle errors thrown by + // both tokenserverclient and hawkclient. + // A tokenserver error thrown based on a bad response. + if (err.response && err.response.status === 401) { + err = new AuthenticationError(err); + // A hawkclient error. + } else if (err.code === 401) { + err = new AuthenticationError(err); + } + // TODO: write tests to make sure that different auth error cases are handled here // properly: auth error getting assertion, auth error getting token (invalid generation // and client-state error) @@ -535,6 +539,7 @@ this.BrowserIDManager.prototype = { // current user stored in this._token. When resolved, this._token is valid. _ensureValidToken: function() { if (this.hasValidToken()) { + this._log.debug("_ensureValidToken already has one"); return Promise.resolve(); } return this._fetchTokenForUser().then( @@ -623,15 +628,25 @@ BrowserIDClusterManager.prototype = { if (!endpoint.endsWith("/")) { endpoint += "/"; } + log.debug("_findCluster returning " + endpoint); return endpoint; }.bind(this); // Spinningly ensure we are ready to authenticate and have a valid token. let promiseClusterURL = function() { return this.identity.whenReadyToAuthenticate.promise.then( - () => this.identity._ensureValidToken() - ).then( - () => endPointFromIdentityToken() + () => { + // We need to handle node reassignment here. If we are being asked + // for a clusterURL while the service already has a clusterURL, then + // it's likely a 401 was received using the existing token - in which + // case we just discard the existing token and fetch a new one. + if (this.service.clusterURL) { + log.debug("_findCluster found existing clusterURL, so discarding the current token"); + this.identity._token = null; + } + return this.identity._ensureValidToken(); + } + ).then(endPointFromIdentityToken ); }.bind(this); diff --git a/services/sync/modules/policies.js b/services/sync/modules/policies.js index 953a6265565..6dfaf40242d 100644 --- a/services/sync/modules/policies.js +++ b/services/sync/modules/policies.js @@ -94,7 +94,7 @@ SyncScheduler.prototype = { Svc.Obs.add("weave:engine:sync:applied", this); Svc.Obs.add("weave:service:setup-complete", this); Svc.Obs.add("weave:service:start-over", this); - Svc.Obs.add("tokenserver:backoff:interval", this); + Svc.Obs.add("FxA:hawk:backoff:interval", this); if (Status.checkSetup() == STATUS_OK) { Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime")); @@ -184,7 +184,7 @@ SyncScheduler.prototype = { this.nextSync = 0; this.handleSyncError(); break; - case "tokenserver:backoff:interval": + case "FxA:hawk:backoff:interval": case "weave:service:backoff:interval": let requested_interval = subject * 1000; this._log.debug("Got backoff notification: " + requested_interval + "ms"); diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 814ce84912d..872b6393eae 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -656,7 +656,7 @@ Sync11Service.prototype = { } }, - verifyLogin: function verifyLogin() { + verifyLogin: function verifyLogin(allow40XRecovery = true) { // If the identity isn't ready it might not know the username... if (!this.identity.readyToAuthenticate) { this._log.info("Not ready to authenticate in verifyLogin."); @@ -727,8 +727,8 @@ Sync11Service.prototype = { case 404: // Check that we're verifying with the correct cluster - if (this._clusterManager.setCluster()) { - return this.verifyLogin(); + if (allow40XRecovery && this._clusterManager.setCluster()) { + return this.verifyLogin(false); } // We must have the right cluster, but the server doesn't expect us @@ -986,10 +986,9 @@ Sync11Service.prototype = { }, logout: function logout() { - // No need to do anything if we're already logged out. - if (!this._loggedIn) - return; - + // If we failed during login, we aren't going to have this._loggedIn set, + // but we still want to ask the identity to logout, so it doesn't try and + // reuse any old credentials next time we sync. this._log.info("Logging out"); this.identity.logout(); this._loggedIn = false; @@ -1059,6 +1058,14 @@ Sync11Service.prototype = { // ... fetch the current record from the server, and COPY THE FLAGS. let newMeta = this.recordManager.get(this.metaURL); + // If we got a 401, we do not want to create a new meta/global - we + // should be able to get the existing meta after we get a new node. + if (this.recordManager.response.status == 401) { + this._log.debug("Fetching meta/global record on the server returned 401."); + this.errorHandler.checkServerError(this.recordManager.response); + return false; + } + if (!this.recordManager.response.success || !newMeta) { this._log.debug("No meta/global record on the server. Creating one."); newMeta = new WBORecord("meta", "global"); diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js index e4b39b88623..3fe3d951327 100644 --- a/services/sync/tests/unit/head_http_server.js +++ b/services/sync/tests/unit/head_http_server.js @@ -806,7 +806,12 @@ SyncServer.prototype = { } let [all, version, username, first, rest] = parts; - if (version != SYNC_API_VERSION) { + // Doing a float compare of the version allows for us to pretend there was + // a node-reassignment - eg, we could re-assign from "1.1/user/" to + // "1.10/user" - this server will then still accept requests with the new + // URL while any code in sync itself which compares URLs will see a + // different URL. + if (parseFloat(version) != parseFloat(SYNC_API_VERSION)) { this._log.debug("SyncServer: Unknown version."); throw HTTP_404; } diff --git a/services/sync/tests/unit/test_browserid_identity.js b/services/sync/tests/unit/test_browserid_identity.js index 62446f6f24a..3556153efd6 100644 --- a/services/sync/tests/unit/test_browserid_identity.js +++ b/services/sync/tests/unit/test_browserid_identity.js @@ -408,14 +408,87 @@ add_task(function test_getTokenErrorWithRetry() { Assert.ok(Status.backoffInterval >= 200000); }); +add_task(function test_getKeysErrorWithBackoff() { + _("Auth server (via hawk) sends an observer notification on backoff headers."); + + // Set Sync's backoffInterval to zero - after we simulated the backoff header + // it should reflect the value we sent. + Status.backoffInterval = 0; + _("Arrange for a 503 with a X-Backoff header."); + + let config = makeIdentityConfig(); + // We want no kA or kB so we attempt to fetch them. + delete config.fxaccount.user.kA; + delete config.fxaccount.user.kB; + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "get"); + Assert.equal(uri, "http://mockedserver:9999/account/keys") + return { + status: 503, + headers: {"content-type": "application/json", + "x-backoff": "100"}, + body: "{}", + } + }); + + let browseridManager = Service.identity; + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to 503"); + + // The observer should have fired - check it got the value in the response. + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); + // Sync will have the value in ms with some slop - so check it is at least that. + Assert.ok(Status.backoffInterval >= 100000); +}); + +add_task(function test_getKeysErrorWithRetry() { + _("Auth server (via hawk) sends an observer notification on retry headers."); + + // Set Sync's backoffInterval to zero - after we simulated the backoff header + // it should reflect the value we sent. + Status.backoffInterval = 0; + _("Arrange for a 503 with a Retry-After header."); + + let config = makeIdentityConfig(); + // We want no kA or kB so we attempt to fetch them. + delete config.fxaccount.user.kA; + delete config.fxaccount.user.kB; + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "get"); + Assert.equal(uri, "http://mockedserver:9999/account/keys") + return { + status: 503, + headers: {"content-type": "application/json", + "retry-after": "100"}, + body: "{}", + } + }); + + let browseridManager = Service.identity; + yield Assert_rejects(browseridManager.whenReadyToAuthenticate.promise, + "should reject due to 503"); + + // The observer should have fired - check it got the value in the response. + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected"); + // Sync will have the value in ms with some slop - so check it is at least that. + Assert.ok(Status.backoffInterval >= 100000); +}); + add_task(function test_getHAWKErrors() { _("BrowserIDManager correctly handles various HAWK failures."); _("Arrange for a 401 - Sync should reflect an auth error."); - yield initializeIdentityWithHAWKFailure({ - status: 401, - headers: {"content-type": "application/json"}, - body: JSON.stringify({}), + let config = makeIdentityConfig(); + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "post"); + Assert.equal(uri, "http://mockedserver:9999/certificate/sign") + return { + status: 401, + headers: {"content-type": "application/json"}, + body: JSON.stringify({}), + } }); Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); @@ -424,16 +497,62 @@ add_task(function test_getHAWKErrors() { // And for good measure, some totally "unexpected" errors - we generally // assume these problems are going to magically go away at some point. _("Arrange for an empty body with a 200 response - should reflect a network error."); - yield initializeIdentityWithHAWKFailure({ - status: 200, - headers: [], - body: "", + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "post"); + Assert.equal(uri, "http://mockedserver:9999/certificate/sign") + return { + status: 200, + headers: [], + body: "", + } }); Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR"); }); -add_task(function test_getKeysError() { - _("BrowserIDManager correctly handles getKeys failures."); +add_task(function test_getGetKeysFailing401() { + _("BrowserIDManager correctly handles 401 responses fetching keys."); + + _("Arrange for a 401 - Sync should reflect an auth error."); + let config = makeIdentityConfig(); + // We want no kA or kB so we attempt to fetch them. + delete config.fxaccount.user.kA; + delete config.fxaccount.user.kB; + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "get"); + Assert.equal(uri, "http://mockedserver:9999/account/keys") + return { + status: 401, + headers: {"content-type": "application/json"}, + body: "{}", + } + }); + Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected"); +}); + +add_task(function test_getGetKeysFailing503() { + _("BrowserIDManager correctly handles 5XX responses fetching keys."); + + _("Arrange for a 503 - Sync should reflect a network error."); + let config = makeIdentityConfig(); + // We want no kA or kB so we attempt to fetch them. + delete config.fxaccount.user.kA; + delete config.fxaccount.user.kB; + config.fxaccount.user.keyFetchToken = "keyfetchtoken"; + yield initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) { + Assert.equal(method, "get"); + Assert.equal(uri, "http://mockedserver:9999/account/keys") + return { + status: 503, + headers: {"content-type": "application/json"}, + body: "{}", + } + }); + Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error"); +}); + +add_task(function test_getKeysMissing() { + _("BrowserIDManager correctly handles getKeys succeeding but not returning keys."); let browseridManager = new BrowserIDManager(); let identityConfig = makeIdentityConfig(); @@ -482,32 +601,44 @@ add_task(function test_getKeysError() { // Utility functions follow // Create a new browserid_identity object and initialize it with a -// hawk mock that simulates a failure. +// hawk mock that simulates HTTP responses. +// The callback function will be called each time the mocked hawk server wants +// to make a request. The result of the callback should be the mock response +// object that will be returned to hawk. // A token server mock will be used that doesn't hit a server, so we move // directly to a hawk request. -function* initializeIdentityWithHAWKFailure(response) { +function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) { // A mock request object. - function MockRESTRequest() {}; + function MockRESTRequest(uri, credentials, extra) { + this._uri = uri; + this._credentials = credentials; + this._extra = extra; + }; MockRESTRequest.prototype = { setHeader: function() {}, post: function(data, callback) { - this.response = response; + this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra); + callback.call(this); + }, + get: function(callback) { + this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra); callback.call(this); } } // The hawk client. function MockedHawkClient() {} - MockedHawkClient.prototype = new HawkClient(); + MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999"); MockedHawkClient.prototype.constructor = MockedHawkClient; MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) { - return new MockRESTRequest(); + return new MockRESTRequest(uri, credentials, extra); } + // Arrange for the same observerPrefix as FxAccountsClient uses + MockedHawkClient.prototype.observerPrefix = "FxA:hawk"; // tie it all together - configureFxAccountIdentity isn't useful here :( let fxaClient = new MockFxAccountsClient(); fxaClient.hawk = new MockedHawkClient(); - let config = makeIdentityConfig(); let internal = { fxAccountsClient: fxaClient, } diff --git a/services/sync/tests/unit/test_fxa_node_reassignment.js b/services/sync/tests/unit/test_fxa_node_reassignment.js new file mode 100644 index 00000000000..2f61afd6fbc --- /dev/null +++ b/services/sync/tests/unit/test_fxa_node_reassignment.js @@ -0,0 +1,321 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +_("Test that node reassignment happens correctly using the FxA identity mgr."); +// The node-reassignment logic is quite different for FxA than for the legacy +// provider. In particular, there's no special request necessary for +// reassignment - it comes from the token server - so we need to ensure the +// Fxa cluster manager grabs a new token. + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://services-common/rest.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/service.js"); +Cu.import("resource://services-sync/status.js"); +Cu.import("resource://services-sync/util.js"); +Cu.import("resource://testing-common/services/sync/rotaryengine.js"); +Cu.import("resource://services-sync/browserid_identity.js"); +Cu.import("resource://testing-common/services/sync/utils.js"); + +Service.engineManager.clear(); + +function run_test() { + Log.repository.getLogger("Sync.AsyncResource").level = Log.Level.Trace; + Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace; + Log.repository.getLogger("Sync.Resource").level = Log.Level.Trace; + Log.repository.getLogger("Sync.RESTRequest").level = Log.Level.Trace; + Log.repository.getLogger("Sync.Service").level = Log.Level.Trace; + Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace; + initTestLogging(); + + Service.engineManager.register(RotaryEngine); + + // Setup the FxA identity manager and cluster manager. + Status.__authManager = Service.identity = new BrowserIDManager(); + Service._clusterManager = Service.identity.createClusterManager(Service); + + // None of the failures in this file should result in a UI error. + function onUIError() { + do_throw("Errors should not be presented in the UI."); + } + Svc.Obs.add("weave:ui:login:error", onUIError); + Svc.Obs.add("weave:ui:sync:error", onUIError); + + run_next_test(); +} + + +// API-compatible with SyncServer handler. Bind `handler` to something to use +// as a ServerCollection handler. +function handleReassign(handler, req, resp) { + resp.setStatusLine(req.httpVersion, 401, "Node reassignment"); + resp.setHeader("Content-Type", "application/json"); + let reassignBody = JSON.stringify({error: "401inator in place"}); + resp.bodyOutputStream.write(reassignBody, reassignBody.length); +} + +let numTokenRequests = 0; + +function prepareServer(cbAfterTokenFetch) { + let config = makeIdentityConfig({username: "johndoe"}); + let server = new SyncServer(); + server.registerUser("johndoe"); + server.start(); + + // Set the token endpoint for the initial token request that's done implicitly + // via configureIdentity. + config.fxaccount.token.endpoint = server.baseURI + "1.1/johndoe"; + // And future token fetches will do magic around numReassigns. + let numReassigns = 0; + return configureIdentity(config).then(() => { + Service.identity._tokenServerClient = { + getTokenFromBrowserIDAssertion: function(uri, assertion, cb) { + // Build a new URL with trailing zeros for the SYNC_VERSION part - this + // will still be seen as equivalent by the test server, but different + // by sync itself. + numReassigns += 1; + let trailingZeros = new Array(numReassigns + 1).join('0'); + let token = config.fxaccount.token; + token.endpoint = server.baseURI + "1.1" + trailingZeros + "/johndoe"; + token.uid = config.username; + numTokenRequests += 1; + cb(null, token); + if (cbAfterTokenFetch) { + cbAfterTokenFetch(); + } + }, + }; + Service.clusterURL = config.fxaccount.token.endpoint; + return server; + }); +} + +function getReassigned() { + try { + return Services.prefs.getBoolPref("services.sync.lastSyncReassigned"); + } catch (ex if (ex.result == Cr.NS_ERROR_UNEXPECTED)) { + return false; + } catch (ex) { + do_throw("Got exception retrieving lastSyncReassigned: " + + Utils.exceptionStr(ex)); + } +} + +/** + * Make a test request to `url`, then watch the result of two syncs + * to ensure that a node request was made. + * Runs `between` between the two. This can be used to undo deliberate failure + * setup, detach observers, etc. + */ +function syncAndExpectNodeReassignment(server, firstNotification, between, + secondNotification, url) { + _("Starting syncAndExpectNodeReassignment\n"); + let deferred = Promise.defer(); + function onwards() { + let numTokenRequestsBefore; + function onFirstSync() { + _("First sync completed."); + Svc.Obs.remove(firstNotification, onFirstSync); + Svc.Obs.add(secondNotification, onSecondSync); + + do_check_eq(Service.clusterURL, ""); + + // Track whether we fetched a new token. + numTokenRequestsBefore = numTokenRequests; + + // Allow for tests to clean up error conditions. + between(); + } + function onSecondSync() { + _("Second sync completed."); + Svc.Obs.remove(secondNotification, onSecondSync); + Service.scheduler.clearSyncTriggers(); + + // Make absolutely sure that any event listeners are done with their work + // before we proceed. + waitForZeroTimer(function () { + _("Second sync nextTick."); + do_check_eq(numTokenRequests, numTokenRequestsBefore + 1, "fetched a new token"); + Service.startOver(); + server.stop(deferred.resolve); + }); + } + + Svc.Obs.add(firstNotification, onFirstSync); + Service.sync(); + } + + // Make sure that it works! + _("Making request to " + url + " which should 401"); + let request = new RESTRequest(url); + request.get(function () { + do_check_eq(request.response.status, 401); + Utils.nextTick(onwards); + }); + yield deferred.promise; +} + +add_task(function test_momentary_401_engine() { + _("Test a failure for engine URLs that's resolved by reassignment."); + let server = yield prepareServer(); + let john = server.user("johndoe"); + + _("Enabling the Rotary engine."); + let engine = Service.engineManager.get("rotary"); + engine.enabled = true; + + // We need the server to be correctly set up prior to experimenting. Do this + // through a sync. + let global = {syncID: Service.syncID, + storageVersion: STORAGE_VERSION, + rotary: {version: engine.version, + syncID: engine.syncID}} + john.createCollection("meta").insert("global", global); + + _("First sync to prepare server contents."); + Service.sync(); + + _("Setting up Rotary collection to 401."); + let rotary = john.createCollection("rotary"); + let oldHandler = rotary.collectionHandler; + rotary.collectionHandler = handleReassign.bind(this, undefined); + + // We want to verify that the clusterURL pref has been cleared after a 401 + // inside a sync. Flag the Rotary engine to need syncing. + john.collection("rotary").timestamp += 1000; + + function between() { + _("Undoing test changes."); + rotary.collectionHandler = oldHandler; + + function onLoginStart() { + // lastSyncReassigned shouldn't be cleared until a sync has succeeded. + _("Ensuring that lastSyncReassigned is still set at next sync start."); + Svc.Obs.remove("weave:service:login:start", onLoginStart); + do_check_true(getReassigned()); + } + + _("Adding observer that lastSyncReassigned is still set on login."); + Svc.Obs.add("weave:service:login:start", onLoginStart); + } + + yield syncAndExpectNodeReassignment(server, + "weave:service:sync:finish", + between, + "weave:service:sync:finish", + Service.storageURL + "rotary"); +}); + +// This test ends up being a failing info fetch *after we're already logged in*. +add_task(function test_momentary_401_info_collections_loggedin() { + _("Test a failure for info/collections after login that's resolved by reassignment."); + let server = yield prepareServer(); + + _("First sync to prepare server contents."); + Service.sync(); + + _("Arrange for info/collections to return a 401."); + let oldHandler = server.toplevelHandlers.info; + server.toplevelHandlers.info = handleReassign; + + function undo() { + _("Undoing test changes."); + server.toplevelHandlers.info = oldHandler; + } + + do_check_true(Service.isLoggedIn, "already logged in"); + + yield syncAndExpectNodeReassignment(server, + "weave:service:sync:error", + undo, + "weave:service:sync:finish", + Service.infoURL); +}); + +// This test ends up being a failing info fetch *before we're logged in*. +// In this case we expect to recover during the login phase - so the first +// sync succeeds. +add_task(function test_momentary_401_info_collections_loggedout() { + _("Test a failure for info/collections before login that's resolved by reassignment."); + + let oldHandler; + let sawTokenFetch = false; + + function afterTokenFetch() { + // After a single token fetch, we undo our evil handleReassign hack, so + // the next /info request returns the collection instead of a 401 + server.toplevelHandlers.info = oldHandler; + sawTokenFetch = true; + } + + let server = yield prepareServer(afterTokenFetch); + + // Return a 401 for the next /info request - it will be reset immediately + // after a new token is fetched. + oldHandler = server.toplevelHandlers.info + server.toplevelHandlers.info = handleReassign; + + do_check_false(Service.isLoggedIn, "not already logged in"); + + Service.sync(); + do_check_eq(Status.sync, SYNC_SUCCEEDED, "sync succeeded"); + // sync was successful - check we grabbed a new token. + do_check_true(sawTokenFetch, "a new token was fetched by this test.") + // and we are done. + Service.startOver(); + let deferred = Promise.defer(); + server.stop(deferred.resolve); + yield deferred.promise; +}); + +// This test ends up being a failing meta/global fetch *after we're already logged in*. +add_task(function test_momentary_401_storage_loggedin() { + _("Test a failure for any storage URL after login that's resolved by" + + "reassignment."); + let server = yield prepareServer(); + + _("First sync to prepare server contents."); + Service.sync(); + + _("Arrange for meta/global to return a 401."); + let oldHandler = server.toplevelHandlers.storage; + server.toplevelHandlers.storage = handleReassign; + + function undo() { + _("Undoing test changes."); + server.toplevelHandlers.storage = oldHandler; + } + + do_check_true(Service.isLoggedIn, "already logged in"); + + yield syncAndExpectNodeReassignment(server, + "weave:service:sync:error", + undo, + "weave:service:sync:finish", + Service.storageURL + "meta/global"); +}); + +// This test ends up being a failing meta/global fetch *before we've logged in*. +add_task(function test_momentary_401_storage_loggedout() { + _("Test a failure for any storage URL before login, not just engine parts. " + + "Resolved by reassignment."); + let server = yield prepareServer(); + + // Return a 401 for all storage requests. + let oldHandler = server.toplevelHandlers.storage; + server.toplevelHandlers.storage = handleReassign; + + function undo() { + _("Undoing test changes."); + server.toplevelHandlers.storage = oldHandler; + } + + do_check_false(Service.isLoggedIn, "already logged in"); + + yield syncAndExpectNodeReassignment(server, + "weave:service:login:error", + undo, + "weave:service:sync:finish", + Service.storageURL + "meta/global"); +}); + diff --git a/services/sync/tests/unit/test_node_reassignment.js b/services/sync/tests/unit/test_node_reassignment.js index db093c26ee8..7fe5ed7eda8 100644 --- a/services/sync/tests/unit/test_node_reassignment.js +++ b/services/sync/tests/unit/test_node_reassignment.js @@ -24,6 +24,8 @@ function run_test() { Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace; initTestLogging(); + ensureLegacyIdentityManager(); + Service.engineManager.register(RotaryEngine); // None of the failures in this file should result in a UI error. @@ -158,7 +160,7 @@ function syncAndExpectNodeReassignment(server, firstNotification, between, yield deferred.promise; } -add_identity_test(this, function test_momentary_401_engine() { +add_task(function test_momentary_401_engine() { _("Test a failure for engine URLs that's resolved by reassignment."); let server = yield prepareServer(); let john = server.user("johndoe"); @@ -210,7 +212,7 @@ add_identity_test(this, function test_momentary_401_engine() { }); // This test ends up being a failing fetch *after we're already logged in*. -add_identity_test(this, function test_momentary_401_info_collections() { +add_task(function test_momentary_401_info_collections() { _("Test a failure for info/collections that's resolved by reassignment."); let server = yield prepareServer(); @@ -233,7 +235,32 @@ add_identity_test(this, function test_momentary_401_info_collections() { Service.infoURL); }); -add_identity_test(this, function test_momentary_401_storage() { +add_task(function test_momentary_401_storage_loggedin() { + _("Test a failure for any storage URL, not just engine parts. " + + "Resolved by reassignment."); + let server = yield prepareServer(); + + _("Performing initial sync to ensure we are logged in.") + Service.sync(); + + // Return a 401 for all storage requests. + let oldHandler = server.toplevelHandlers.storage; + server.toplevelHandlers.storage = handleReassign; + + function undo() { + _("Undoing test changes."); + server.toplevelHandlers.storage = oldHandler; + } + + do_check_true(Service.isLoggedIn, "already logged in"); + yield syncAndExpectNodeReassignment(server, + "weave:service:sync:error", + undo, + "weave:service:sync:finish", + Service.storageURL + "meta/global"); +}); + +add_task(function test_momentary_401_storage_loggedout() { _("Test a failure for any storage URL, not just engine parts. " + "Resolved by reassignment."); let server = yield prepareServer(); @@ -247,6 +274,7 @@ add_identity_test(this, function test_momentary_401_storage() { server.toplevelHandlers.storage = oldHandler; } + do_check_false(Service.isLoggedIn, "not already logged in"); yield syncAndExpectNodeReassignment(server, "weave:service:login:error", undo, @@ -254,7 +282,7 @@ add_identity_test(this, function test_momentary_401_storage() { Service.storageURL + "meta/global"); }); -add_identity_test(this, function test_loop_avoidance_storage() { +add_task(function test_loop_avoidance_storage() { _("Test that a repeated failure doesn't result in a sync loop " + "if node reassignment cannot resolve the failure."); @@ -354,7 +382,7 @@ add_identity_test(this, function test_loop_avoidance_storage() { yield deferred.promise; }); -add_identity_test(this, function test_loop_avoidance_engine() { +add_task(function test_loop_avoidance_engine() { _("Test that a repeated 401 in an engine doesn't result in a sync loop " + "if node reassignment cannot resolve the failure."); let server = yield prepareServer(); diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini index 4de9871f31e..fcba6e3ab3d 100644 --- a/services/sync/tests/unit/xpcshell.ini +++ b/services/sync/tests/unit/xpcshell.ini @@ -124,6 +124,7 @@ skip-if = os == "android" # Firefox Accounts specific tests [test_fxa_startOver.js] [test_fxa_service_cluster.js] +[test_fxa_node_reassignment.js] # Finally, we test each engine. [test_addons_engine.js] diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js index 30a7f00b656..ddf278ff287 100644 --- a/testing/profiles/prefs_general.js +++ b/testing/profiles/prefs_general.js @@ -54,6 +54,7 @@ user_pref("font.size.inflation.minTwips", 0); // AddonManager tests require that the experiments feature be enabled. user_pref("experiments.enabled", true); user_pref("experiments.supported", true); +user_pref("experiments.logging.level", "Trace"); // Only load extensions from the application and user profile // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION diff --git a/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm index d8246feda4c..c90c130b21e 100644 --- a/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm +++ b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm @@ -474,10 +474,15 @@ exports.Type = Type; */ let projectLargeInt = function projectLargeInt(x) { - return parseInt(x.toString(), 10); + let str = x.toString(); + let rv = parseInt(str, 10); + if (rv.toString() !== str) { + throw new TypeError("Number " + str + " cannot be projected to a double"); + } + return rv; }; let projectLargeUInt = function projectLargeUInt(x) { - return parseInt(x.toString(), 10); + return projectLargeInt(x); }; let projectValue = function projectValue(x) { if (!(x instanceof ctypes.CData)) { diff --git a/toolkit/components/osfile/modules/osfile_win_back.jsm b/toolkit/components/osfile/modules/osfile_win_back.jsm index a2630816c52..9b7bf22a68c 100644 --- a/toolkit/components/osfile/modules/osfile_win_back.jsm +++ b/toolkit/components/osfile/modules/osfile_win_back.jsm @@ -99,14 +99,17 @@ return SysFile._FindClose; }); - Type.DWORD = Type.int32_t.withName("DWORD"); + Type.DWORD = Type.uint32_t.withName("DWORD"); - /** - * A C integer holding -1 in case of error or a positive integer - * in case of success. + /* A special type used to represent flags passed as DWORDs to a function. + * In JavaScript, bitwise manipulation of numbers, such as or-ing flags, + * can produce negative numbers. Since DWORD is unsigned, these negative + * numbers simply cannot be converted to DWORD. For this reason, whenever + * bit manipulation is called for, you should rather use DWORD_FLAGS, + * which is represented as a signed integer, hence has the correct + * semantics. */ - Type.negative_or_DWORD = - Type.DWORD.withName("negative_or_DWORD"); + Type.DWORD_FLAGS = Type.int32_t.withName("DWORD_FLAGS"); /** * A C integer holding 0 in case of error or a positive integer @@ -241,11 +244,11 @@ "CreateFileW", ctypes.winapi_abi, /*return*/ Type.file_HANDLE, /*name*/ Type.path, - /*access*/ Type.DWORD, - /*share*/ Type.DWORD, + /*access*/ Type.DWORD_FLAGS, + /*share*/ Type.DWORD_FLAGS, /*security*/Type.SECURITY_ATTRIBUTES.in_ptr, - /*creation*/Type.DWORD, - /*flags*/ Type.DWORD, + /*creation*/Type.DWORD_FLAGS, + /*flags*/ Type.DWORD_FLAGS, /*template*/Type.HANDLE); libc.declareLazyFFI(SysFile, "DeleteFile", @@ -280,10 +283,10 @@ libc.declareLazyFFI(SysFile, "FormatMessage", "FormatMessageW", ctypes.winapi_abi, /*return*/ Type.DWORD, - /*flags*/ Type.DWORD, + /*flags*/ Type.DWORD_FLAGS, /*source*/ Type.void_t.in_ptr, - /*msgid*/ Type.DWORD, - /*langid*/ Type.DWORD, + /*msgid*/ Type.DWORD_FLAGS, + /*langid*/ Type.DWORD_FLAGS, /*buf*/ Type.out_wstring, /*size*/ Type.DWORD, /*Arguments*/Type.void_t.in_ptr @@ -346,7 +349,7 @@ libc.declareLazyFFI(SysFile, "SetFilePointer", "SetFilePointer", ctypes.winapi_abi, - /*return*/ Type.negative_or_DWORD, + /*return*/ Type.DWORD, /*file*/ Type.HANDLE, /*distlow*/Type.long, /*disthi*/ Type.long.in_ptr, @@ -378,14 +381,14 @@ libc.declareLazyFFI(SysFile, "GetFileAttributes", "GetFileAttributesW", ctypes.winapi_abi, - /*return*/ Type.DWORD, + /*return*/ Type.DWORD_FLAGS, /*fileName*/ Type.path); libc.declareLazyFFI(SysFile, "SetFileAttributes", "SetFileAttributesW", ctypes.winapi_abi, /*return*/ Type.zero_or_nothing, /*fileName*/ Type.path, - /*fileAttributes*/ Type.DWORD); + /*fileAttributes*/ Type.DWORD_FLAGS); advapi32.declareLazyFFI(SysFile, "GetNamedSecurityInfo", "GetNamedSecurityInfoW", ctypes.winapi_abi, diff --git a/toolkit/components/osfile/modules/osfile_win_front.jsm b/toolkit/components/osfile/modules/osfile_win_front.jsm index 33fe16f632c..b8589644260 100644 --- a/toolkit/components/osfile/modules/osfile_win_front.jsm +++ b/toolkit/components/osfile/modules/osfile_win_front.jsm @@ -35,16 +35,16 @@ // Mutable thread-global data // In the Windows implementation, methods |read| and |write| - // require passing a pointer to an int32 to determine how many + // require passing a pointer to an uint32 to determine how many // bytes have been read/written. In C, this is a benigne operation, // but in js-ctypes, this has a cost. Rather than re-allocating a - // C int32 and a C int32* for each |read|/|write|, we take advantage + // C uint32 and a C uint32* for each |read|/|write|, we take advantage // of the fact that the state is thread-private -- hence that two // |read|/|write| operations cannot take place at the same time -- // and we use the following global mutable values: - let gBytesRead = new ctypes.int32_t(-1); + let gBytesRead = new ctypes.uint32_t(0); let gBytesReadPtr = gBytesRead.address(); - let gBytesWritten = new ctypes.int32_t(-1); + let gBytesWritten = new ctypes.uint32_t(0); let gBytesWrittenPtr = gBytesWritten.address(); // Same story for GetFileInformationByHandle @@ -176,9 +176,24 @@ if (whence === undefined) { whence = Const.FILE_BEGIN; } - return throw_on_negative("setPosition", - WinFile.SetFilePointer(this.fd, pos, null, whence), - this._path); + let pos64 = ctypes.Int64(pos); + // Per MSDN, while |lDistanceToMove| (low) is declared as int32_t, when + // providing |lDistanceToMoveHigh| as well, it should countain the + // bottom 32 bits of the 64-bit integer. Hence the following |posLo| + // cast is OK. + let posLo = new ctypes.uint32_t(ctypes.Int64.lo(pos64)); + posLo = ctypes.cast(posLo, ctypes.int32_t); + let posHi = new ctypes.int32_t(ctypes.Int64.hi(pos64)); + let result = WinFile.SetFilePointer( + this.fd, posLo.value, posHi.address(), whence); + // INVALID_SET_FILE_POINTER might be still a valid result, as it + // represents the lower 32 bit of the int64 result. MSDN says to check + // both, INVALID_SET_FILE_POINTER and a non-zero winLastError. + if (result == Const.INVALID_SET_FILE_POINTER && ctypes.winLastError) { + throw new File.Error("setPosition", ctypes.winLastError, this._path); + } + pos64 = ctypes.Int64.join(posHi.value, result); + return Type.int64_t.project(pos64); }; /** diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js index c7219075618..0efa64704b1 100644 --- a/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js +++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js @@ -103,10 +103,10 @@ function test_ReadWrite() null); isnot(output, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_ReadWrite: output file opened"); let array = new (ctypes.ArrayType(ctypes.char, 4096))(); - let bytes_read = new ctypes.int32_t(-1); + let bytes_read = new ctypes.uint32_t(0); let bytes_read_ptr = bytes_read.address(); log("We have a pointer for bytes read: "+bytes_read_ptr); - let bytes_written = new ctypes.int32_t(-1); + let bytes_written = new ctypes.uint32_t(0); let bytes_written_ptr = bytes_written.address(); log("We have a pointer for bytes written: "+bytes_written_ptr); log("test_ReadWrite: buffer and pointers ready"); @@ -141,7 +141,7 @@ function test_ReadWrite() isnot (result, OS.Constants.Win.INVALID_SET_FILE_POINTER, "test_ReadWrite: output reset"); let array2 = new (ctypes.ArrayType(ctypes.char, 4096))(); - let bytes_read2 = new ctypes.int32_t(-1); + let bytes_read2 = new ctypes.uint32_t(0); let bytes_read2_ptr = bytes_read2.address(); let pos = 0; while (true) { diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js new file mode 100644 index 00000000000..31085fa1662 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js @@ -0,0 +1,153 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Components.utils.import("resource://gre/modules/ctypes.jsm"); +Components.utils.import("resource://gre/modules/osfile.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); + +/** + * A test to check that .getPosition/.setPosition work with large files. + * (see bug 952997) + */ + +// Test setPosition/getPosition. +function test_setPosition(forward, current, backward) { + let path = OS.Path.join(OS.Constants.Path.tmpDir, + "test_osfile_async_largefiles.tmp"); + + // Clear any left-over files from previous runs. + try { + yield OS.File.remove(path); + } catch (ex if ex.becauseNoSuchFile) { + // ignore + } + + try { + let file = yield OS.File.open(path, {write:true, append:false}); + try { + let pos = 0; + + // 1. seek forward from start + do_print("Moving forward: " + forward); + yield file.setPosition(forward, OS.File.POS_START); + pos += forward; + do_check_eq((yield file.getPosition()), pos); + + // 2. seek forward from current position + do_print("Moving current: " + current); + yield file.setPosition(current, OS.File.POS_CURRENT); + pos += current; + do_check_eq((yield file.getPosition()), pos); + + // 3. seek backward from current position + do_print("Moving current backward: " + backward); + yield file.setPosition(-backward, OS.File.POS_CURRENT); + pos -= backward; + do_check_eq((yield file.getPosition()), pos); + + } finally { + yield file.setPosition(0, OS.File.POS_START); + yield file.close(); + } + } catch(ex) { + try { + yield OS.File.remove(path); + } catch (ex if ex.becauseNoSuchFile) { + // ignore. + } + do_throw(ex); + } +} + +// Test setPosition/getPosition expected failures. +function test_setPosition_failures() { + let path = OS.Path.join(OS.Constants.Path.tmpDir, + "test_osfile_async_largefiles.tmp"); + + // Clear any left-over files from previous runs. + try { + yield OS.File.remove(path); + } catch (ex if ex.becauseNoSuchFile) { + // ignore + } + + try { + let file = yield OS.File.open(path, {write:true, append:false}); + try { + let pos = 0; + + // 1. Use an invalid position value + try { + yield file.setPosition(0.5, OS.File.POS_START); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + do_check_true(ex.toString().contains("expected type")); + } + // Since setPosition should have bailed, it shouldn't have moved the + // file pointer at all. + do_check_eq((yield file.getPosition()), 0); + + // 2. Use an invalid position value + try { + yield file.setPosition(0xffffffff + 0.5, OS.File.POS_START); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + do_check_true(ex.toString().contains("expected type")); + } + // Since setPosition should have bailed, it shouldn't have moved the + // file pointer at all. + do_check_eq((yield file.getPosition()), 0); + + // 3. Use a position that cannot be represented as a double + try { + // Not all numbers after 9007199254740992 can be represented as a + // double. E.g. in js 9007199254740992 + 1 == 9007199254740992 + yield file.setPosition(9007199254740992, OS.File.POS_START); + yield file.setPosition(1, OS.File.POS_CURRENT); + do_throw("Shouldn't have succeeded"); + } catch (ex) { + do_print(ex.toString()); + do_check_true(!!ex); + } + + } finally { + yield file.setPosition(0, OS.File.POS_START); + yield file.close(); + try { + yield OS.File.remove(path); + } catch (ex if ex.becauseNoSuchFile) { + // ignore. + } + } + } catch(ex) { + do_throw(ex); + } +} + +function run_test() { + // First verify stuff works for small values. + add_task(test_setPosition.bind(null, 0, 100, 50)); + add_task(test_setPosition.bind(null, 1000, 100, 50)); + add_task(test_setPosition.bind(null, 1000, -100, -50)); + + if (OS.Constants.Win || ctypes.off_t.size >= 8) { + // Now verify stuff still works for large values. + // 1. Multiple small seeks, which add up to > MAXINT32 + add_task(test_setPosition.bind(null, 0x7fffffff, 0x7fffffff, 0)); + // 2. Plain large seek, that should end up at 0 again. + // 0xffffffff also happens to be the INVALID_SET_FILE_POINTER value on + // Windows, so this also tests the error handling + add_task(test_setPosition.bind(null, 0, 0xffffffff, 0xffffffff)); + // 3. Multiple large seeks that should end up > MAXINT32. + add_task(test_setPosition.bind(null, 0xffffffff, 0xffffffff, 0xffffffff)); + // 5. Multiple large seeks with negative offsets. + add_task(test_setPosition.bind(null, 0xffffffff, -0x7fffffff, 0x7fffffff)); + + // 6. Check failures + add_task(test_setPosition_failures); + } + + run_next_test(); +} diff --git a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini index 8fc1bc3dd47..fc731cd5944 100644 --- a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini +++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini @@ -10,6 +10,7 @@ tail = [test_osfile_async_bytes.js] [test_osfile_async_copy.js] [test_osfile_async_flush.js] +[test_osfile_async_largefiles.js] [test_osfile_async_setDates.js] [test_removeEmptyDir.js] [test_makeDir.js] diff --git a/toolkit/components/prompts/src/nsPrompter.js b/toolkit/components/prompts/src/nsPrompter.js index eafc91ce613..5988d7b7e17 100644 --- a/toolkit/components/prompts/src/nsPrompter.js +++ b/toolkit/components/prompts/src/nsPrompter.js @@ -389,6 +389,8 @@ function openTabPrompt(domWin, tabPrompt, args) { if (newPrompt) tabPrompt.removePrompt(newPrompt); + domWin.removeEventListener("pagehide", pagehide); + winUtils.leaveModalState(); PromptUtils.fireDialogEvent(domWin, "DOMModalDialogClosed"); diff --git a/toolkit/components/startup/tests/browser/browser.ini b/toolkit/components/startup/tests/browser/browser.ini index 40e25add07c..2204935bc90 100644 --- a/toolkit/components/startup/tests/browser/browser.ini +++ b/toolkit/components/startup/tests/browser/browser.ini @@ -1,5 +1,7 @@ [DEFAULT] -support-files = beforeunload.html +support-files = + head.js + beforeunload.html [browser_bug511456.js] skip-if = e10s # Bug ?????? - test touches content (uses a WindowWatcher in the parent process to try and observe content created alerts etc) diff --git a/toolkit/components/startup/tests/browser/browser_bug511456.js b/toolkit/components/startup/tests/browser/browser_bug511456.js index 1ceb5c912a0..4eb97a8732c 100644 --- a/toolkit/components/startup/tests/browser/browser_bug511456.js +++ b/toolkit/components/startup/tests/browser/browser_bug511456.js @@ -2,87 +2,44 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -const PROMPT_URL = "chrome://global/content/commonDialog.xul"; +"use strict"; + const TEST_URL = "http://example.com/browser/toolkit/components/startup/tests/browser/beforeunload.html"; -var Watcher = { - seen: false, - allowClose: false, - - // Window open handling - windowLoad: function(win) { - // Allow any other load handlers to execute - var self = this; - executeSoon(function() { self.windowReady(win); } ); - }, - - windowReady: function(win) { - if (win.document.location.href != PROMPT_URL) - return; - this.seen = true; - if (this.allowClose) - win.document.documentElement.acceptDialog(); - else - win.document.documentElement.cancelDialog(); - }, - - // nsIWindowMediatorListener - - onWindowTitleChange: function(win, title) { - }, - - onOpenWindow: function(win) { - var domwindow = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindow); - var self = this; - domwindow.addEventListener("load", function() { - domwindow.removeEventListener("load", arguments.callee, false); - self.windowLoad(domwindow); - }, false); - }, - - onCloseWindow: function(win) { - }, - - QueryInterface: function(iid) { - if (iid.equals(Components.interfaces.nsIWindowMediatorListener) || - iid.equals(Components.interfaces.nsISupports)) - return this; - - throw Components.results.NS_ERROR_NO_INTERFACE; - } -} - function test() { waitForExplicitFinish(); ignoreAllUncaughtExceptions(); - Services.wm.addListener(Watcher); - - var win2 = window.openDialog(location, "", "chrome,all,dialog=no", "about:blank"); - win2.addEventListener("load", function() { - win2.removeEventListener("load", arguments.callee, false); + let win2 = window.openDialog(location, "", "chrome,all,dialog=no", "about:blank"); + win2.addEventListener("load", function onLoad() { + win2.removeEventListener("load", onLoad); gBrowser.selectedTab = gBrowser.addTab(TEST_URL); - gBrowser.selectedBrowser.addEventListener("DOMContentLoaded", function() { - if (window.content.location.href != TEST_URL) - return; - gBrowser.selectedBrowser.removeEventListener("DOMContentLoaded", arguments.callee, false); - Watcher.seen = false; - var appStartup = Cc['@mozilla.org/toolkit/app-startup;1']. - getService(Ci.nsIAppStartup); + let browser = gBrowser.selectedBrowser; + + whenBrowserLoaded(browser, function () { + let seenDialog = false; + + // Cancel the prompt the first time. + waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => { + seenDialog = true; + btnStay.click(); + }); + + let appStartup = Cc['@mozilla.org/toolkit/app-startup;1']. + getService(Ci.nsIAppStartup); appStartup.quit(Ci.nsIAppStartup.eAttemptQuit); - Watcher.allowClose = true; - ok(Watcher.seen, "Should have seen a prompt dialog"); + ok(seenDialog, "Should have seen a prompt dialog"); ok(!win2.closed, "Shouldn't have closed the additional window"); win2.close(); - gBrowser.removeTab(gBrowser.selectedTab); - executeSoon(finish_test); - }, false); - }, false); -} -function finish_test() { - Services.wm.removeListener(Watcher); - finish(); + // Leave the page the second time. + waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => { + btnLeave.click(); + }); + + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(finish); + }); + }); } diff --git a/toolkit/components/startup/tests/browser/browser_bug537449.js b/toolkit/components/startup/tests/browser/browser_bug537449.js index e46a88d7a56..280fa2efaf4 100644 --- a/toolkit/components/startup/tests/browser/browser_bug537449.js +++ b/toolkit/components/startup/tests/browser/browser_bug537449.js @@ -2,87 +2,44 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -const PROMPT_URL = "chrome://global/content/commonDialog.xul"; +"use strict"; + const TEST_URL = "http://example.com/browser/toolkit/components/startup/tests/browser/beforeunload.html"; -var Watcher = { - seen: false, - allowClose: false, - - // Window open handling - windowLoad: function(win) { - // Allow any other load handlers to execute - var self = this; - executeSoon(function() { self.windowReady(win); } ); - }, - - windowReady: function(win) { - if (win.document.location.href != PROMPT_URL) - return; - this.seen = true; - if (this.allowClose) - win.document.documentElement.acceptDialog(); - else - win.document.documentElement.cancelDialog(); - }, - - // nsIWindowMediatorListener - - onWindowTitleChange: function(win, title) { - }, - - onOpenWindow: function(win) { - var domwindow = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindow); - var self = this; - domwindow.addEventListener("load", function() { - domwindow.removeEventListener("load", arguments.callee, false); - self.windowLoad(domwindow); - }, false); - }, - - onCloseWindow: function(win) { - }, - - QueryInterface: function(iid) { - if (iid.equals(Components.interfaces.nsIWindowMediatorListener) || - iid.equals(Components.interfaces.nsISupports)) - return this; - - throw Components.results.NS_ERROR_NO_INTERFACE; - } -} - function test() { waitForExplicitFinish(); - Services.wm.addListener(Watcher); - gBrowser.selectedTab = gBrowser.addTab(TEST_URL); - gBrowser.selectedBrowser.addEventListener("DOMContentLoaded", function() { - if (window.content.location.href != TEST_URL) - return; - gBrowser.selectedBrowser.removeEventListener("DOMContentLoaded", arguments.callee, false); - Watcher.seen = false; - var appStartup = Cc['@mozilla.org/toolkit/app-startup;1']. - getService(Ci.nsIAppStartup); + let browser = gBrowser.selectedBrowser; + + whenBrowserLoaded(browser, function () { + let seenDialog = false; + + // Cancel the prompt the first time. + waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => { + seenDialog = true; + btnStay.click(); + }); + + let appStartup = Cc['@mozilla.org/toolkit/app-startup;1']. + getService(Ci.nsIAppStartup); appStartup.quit(Ci.nsIAppStartup.eAttemptQuit); - Watcher.allowClose = true; - ok(Watcher.seen, "Should have seen a prompt dialog"); + ok(seenDialog, "Should have seen a prompt dialog"); ok(!window.closed, "Shouldn't have closed the window"); - var win2 = window.openDialog(location, "", "chrome,all,dialog=no", "about:blank"); + let win2 = window.openDialog(location, "", "chrome,all,dialog=no", "about:blank"); ok(win2 != null, "Should have been able to open a new window"); - win2.addEventListener("load", function() { - win2.removeEventListener("load", arguments.callee, false); + win2.addEventListener("load", function onLoad() { + win2.removeEventListener("load", onLoad); win2.close(); - gBrowser.removeTab(gBrowser.selectedTab); - executeSoon(finish_test); - }, false); - }, false); -} -function finish_test() { - Services.wm.removeListener(Watcher); - finish(); + // Leave the page the second time. + waitForOnBeforeUnloadDialog(browser, (btnLeave, btnStay) => { + btnLeave.click(); + }); + + gBrowser.removeTab(gBrowser.selectedTab); + executeSoon(finish); + }); + }); } diff --git a/toolkit/components/startup/tests/browser/head.js b/toolkit/components/startup/tests/browser/head.js new file mode 100644 index 00000000000..d9e3c9a8be2 --- /dev/null +++ b/toolkit/components/startup/tests/browser/head.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +function whenBrowserLoaded(browser, callback) { + browser.addEventListener("load", function onLoad(event) { + if (event.target == browser.contentDocument) { + browser.removeEventListener("load", onLoad, true); + executeSoon(callback); + } + }, true); +} + +function waitForOnBeforeUnloadDialog(browser, callback) { + browser.addEventListener("DOMWillOpenModalDialog", function onModalDialog() { + browser.removeEventListener("DOMWillOpenModalDialog", onModalDialog, true); + + executeSoon(() => { + let stack = browser.parentNode; + let dialogs = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"); + let {button0, button1} = dialogs[0].ui; + callback(button0, button1); + }); + }, true); +} diff --git a/toolkit/devtools/server/tests/mochitest/test_connectToChild.html b/toolkit/devtools/server/tests/mochitest/test_connectToChild.html index 3d7f2c3cd1c..b315e4f17e8 100644 --- a/toolkit/devtools/server/tests/mochitest/test_connectToChild.html +++ b/toolkit/devtools/server/tests/mochitest/test_connectToChild.html @@ -23,10 +23,11 @@ Cu.import("resource://gre/modules/devtools/dbg-server.jsm"); window.onload = function() { SimpleTest.waitForExplicitFinish(); - // Always log packets when running tests. SpecialPowers.pushPrefEnv({ "set": [ - ["devtools.debugger.log", true] + // Always log packets when running tests. + ["devtools.debugger.log", true], + ["dom.mozBrowserFramesEnabled", true] ] }, runTests); } @@ -34,7 +35,7 @@ window.onload = function() { function runTests() { // Create a minimal iframe with a message manager let iframe = document.createElement("iframe"); - iframe.setAttribute("mozbrowser", "true"); + iframe.mozbrowser = true; document.body.appendChild(iframe); let mm = iframe.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; diff --git a/toolkit/modules/Task.jsm b/toolkit/modules/Task.jsm index 5031b4c5315..4e80a09e91e 100644 --- a/toolkit/modules/Task.jsm +++ b/toolkit/modules/Task.jsm @@ -91,7 +91,7 @@ const Ci = Components.interfaces; const Cu = Components.utils; const Cr = Components.results; -Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +Cu.import("resource://gre/modules/Promise.jsm"); // The following error types are considered programmer errors, which should be // reported (possibly redundantly) so as to let programmers fix their code. diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index c62a6b041c9..3a8772debe3 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -6289,9 +6289,11 @@ function AddonWrapper(aAddon) { ["sourceURI", "releaseNotesURI"].forEach(function(aProp) { this.__defineGetter__(aProp, function AddonWrapper_URIPropertyGetter() { - let target = chooseValue(aAddon, aProp)[0]; + let [target, fromRepo] = chooseValue(aAddon, aProp); if (!target) return null; + if (fromRepo) + return target; return NetUtil.newURI(target); }); }, this); diff --git a/toolkit/mozapps/extensions/test/browser/browser-common.ini b/toolkit/mozapps/extensions/test/browser/browser-common.ini index a9a95b497b5..d3752b47535 100644 --- a/toolkit/mozapps/extensions/test/browser/browser-common.ini +++ b/toolkit/mozapps/extensions/test/browser/browser-common.ini @@ -12,6 +12,7 @@ skip-if = e10s # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions n [browser_bug562899.js] [browser_bug562992.js] [browser_bug567127.js] +skip-if = os == 'win' || os == 'mac' # Bug 608820 [browser_bug567137.js] [browser_bug570760.js] skip-if = e10s # Bug ?????? - EventUtils.synthesizeKey not e10s friendly diff --git a/toolkit/mozapps/extensions/test/browser/browser_experiments.js b/toolkit/mozapps/extensions/test/browser/browser_experiments.js index adeb35669c0..730b52de777 100644 --- a/toolkit/mozapps/extensions/test/browser/browser_experiments.js +++ b/toolkit/mozapps/extensions/test/browser/browser_experiments.js @@ -17,12 +17,18 @@ function test() { // The Experiments Manager will interfere with us by preventing installs // of experiments it doesn't know about. We remove it from the equation // because here we are only concerned with core Addon Manager operation, - // not the super set Experiments Manager has imposed. + // not the superset Experiments Manager has imposed. if ("@mozilla.org/browser/experiments-service;1" in Components.classes) { Components.utils.import("resource:///modules/experiments/Experiments.jsm", gContext); - gContext.Experiments.instance()._stopWatchingAddons(); + + // There is a race condition between XPCOM service initialization and + // this test running. We have to initialize the instance first, then + // uninitialize it to prevent this. + let instance = gContext.Experiments.instance(); + instance.uninit().then(run_next_test); + } else { + run_next_test(); } - run_next_test(); }); } @@ -33,10 +39,11 @@ function end_test() { close_manager(gManagerWindow, () => { if ("@mozilla.org/browser/experiments-service;1" in Components.classes) { - gContext.Experiments.instance()._startWatchingAddons(); + gContext.Experiments.instance().init(); + finish(); + } else { + finish(); } - - finish(); }); } diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_sourceURI.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_sourceURI.xml new file mode 100644 index 00000000000..949288e3f17 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_sourceURI.xml @@ -0,0 +1,18 @@ + + + + + Test + Extension + addon@tests.mozilla.org + 1 + + + xpcshell@tests.mozilla.org + 1 + 1 + + + http://www.example.com/testaddon.xpi + + diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_sourceURI.js b/toolkit/mozapps/extensions/test/xpcshell/test_sourceURI.js new file mode 100644 index 00000000000..e78bb50745c --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_sourceURI.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +Components.utils.import("resource://testing-common/httpd.js"); +var gServer = new HttpServer(); +gServer.start(-1); + +const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled"; + +const PORT = gServer.identity.primaryPort; +const BASE_URL = "http://localhost:" + PORT; +const DEFAULT_URL = "about:blank"; + +var addon = { + id: "addon@tests.mozilla.org", + version: "1.0", + name: "Test", + targetApplications: [{ + id: "xpcshell@tests.mozilla.org", + minVersion: "1", + maxVersion: "1" + }] +}; + +const profileDir = gProfD.clone(); +profileDir.append("extensions"); + +function backgroundUpdate(aCallback) { + Services.obs.addObserver(function() { + Services.obs.removeObserver(arguments.callee, "addons-background-update-complete"); + aCallback(); + }, "addons-background-update-complete", false); + + AddonManagerPrivate.backgroundUpdateCheck(); +} + +function run_test() { + do_test_pending(); + + mapUrlToFile("/cache.xml", do_get_file("data/test_sourceURI.xml"), gServer); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, BASE_URL + "/cache.xml"); + Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE, BASE_URL + "/cache.xml"); + Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true); + + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1"); + writeInstallRDFForExtension(addon, profileDir); + startupManager(); + + AddonManager.getAddonByID("addon@tests.mozilla.org", function(a) { + do_check_neq(a, null); + do_check_eq(a.sourceURI, null); + + backgroundUpdate(function() { + restartManager(); + + AddonManager.getAddonByID("addon@tests.mozilla.org", function(a) { + do_check_neq(a, null); + do_check_neq(a.sourceURI, null); + do_check_eq(a.sourceURI.spec, "http://www.example.com/testaddon.xpi"); + + do_test_finished(); + }); + }); + }); +} diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini index 644dbdb128b..8c6cc19bc6b 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini @@ -262,3 +262,4 @@ run-sequentially = Uses global XCurProcD dir. # Bug 676992: test consistently hangs on Android skip-if = os == "android" run-sequentially = Uses global XCurProcD dir. +[test_sourceURI.js] diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser.ini b/toolkit/mozapps/extensions/test/xpinstall/browser.ini index 68f37c459df..1712670a7d7 100644 --- a/toolkit/mozapps/extensions/test/xpinstall/browser.ini +++ b/toolkit/mozapps/extensions/test/xpinstall/browser.ini @@ -88,3 +88,4 @@ support-files = [browser_whitelist5.js] [browser_whitelist6.js] [browser_whitelist7.js] +skip-if = (os == 'win' || os == 'mac') && debug # bug 986458 - leaked 1 docshell until shutdown on chunked debug bc diff --git a/tools/mercurial/hgsetup/config.py b/tools/mercurial/hgsetup/config.py index 6b9644b1921..52a9cac755c 100644 --- a/tools/mercurial/hgsetup/config.py +++ b/tools/mercurial/hgsetup/config.py @@ -10,7 +10,7 @@ import os HOST_FINGERPRINTS = { - 'bitbucket.org': '67:b3:bf:9f:c5:38:0e:4c:dd:4e:8a:da:3d:11:1b:c2:a5:d1:6c:6b', + 'bitbucket.org': '45:ad:ae:1a:cf:0e:73:47:06:07:e0:88:f5:cc:10:e5:fa:1c:f7:99', 'bugzilla.mozilla.org': '47:13:a2:14:0c:46:45:53:12:0d:e5:36:16:a5:60:26:3e:da:3a:60', 'hg.mozilla.org': 'af:27:b9:34:47:4e:e5:98:01:f6:83:2b:51:c9:aa:d8:df:fb:1a:27', }