diff --git a/browser/base/content/abouthome/aboutHome.js b/browser/base/content/abouthome/aboutHome.js index 26f77a794ba..7ad7d68c978 100644 --- a/browser/base/content/abouthome/aboutHome.js +++ b/browser/base/content/abouthome/aboutHome.js @@ -369,7 +369,7 @@ function showDefaultSnippets() } function fitToWidth() { - if (window.scrollMaxX != window.scrollMinX) { + if (document.documentElement.scrollWidth > window.innerWidth) { document.body.setAttribute("narrow", "true"); } else if (document.body.hasAttribute("narrow")) { document.body.removeAttribute("narrow"); diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index a840ff89e89..658eb22f490 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -31,6 +31,7 @@ nsContextMenu.prototype = { return; this.hasPageMenu = false; + this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed; if (!aIsShift) { if (this.isRemote) { this.hasPageMenu = @@ -71,6 +72,8 @@ nsContextMenu.prototype = { Ci.nsIPrefLocalizedString).data; } catch (e) { } + // Reset after "on-build-contextmenu" notification in case selection was + // changed during the notification. this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed; this.onPlainTextLink = false; diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 9e7dea9b25d..f6f191e87d0 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -2936,6 +2936,62 @@ + + + + + + + + + + + { + if (extension.uninstallURL) { + let browser = Services.wm.getMostRecentWindow("navigator:browser").gBrowser; + browser.addTab(extension.uninstallURL, { relatedToCurrent: true }); + } +}); + diff --git a/browser/components/extensions/ext-tabs.js b/browser/components/extensions/ext-tabs.js index cef8a3fce50..72a43e787c2 100644 --- a/browser/components/extensions/ext-tabs.js +++ b/browser/components/extensions/ext-tabs.js @@ -613,41 +613,33 @@ extensions.registerSchemaAPI("tabs", null, (extension, context) => { // If the window is not specified, use the window from the tab. let window = destinationWindow || tab.ownerDocument.defaultView; - let windowId = WindowManager.getId(window); let gBrowser = window.gBrowser; - let getInsertionPoint = () => { - let point = indexMap.get(window) || index; - // If the index is -1 it should go to the end of the tabs. - if (point == -1) { - point = gBrowser.tabs.length; - } - indexMap.set(window, point + 1); - return point; - }; + let insertionPoint = indexMap.get(window) || index; + // If the index is -1 it should go to the end of the tabs. + if (insertionPoint == -1) { + insertionPoint = gBrowser.tabs.length; + } - if (WindowManager.getId(tab.ownerDocument.defaultView) !== windowId) { + // We can only move pinned tabs to a point within, or just after, + // the current set of pinned tabs. Unpinned tabs, likewise, can only + // be moved to a position after the current set of pinned tabs. + // Attempts to move a tab to an illegal position are ignored. + let numPinned = gBrowser._numPinnedTabs; + let ok = tab.pinned ? insertionPoint <= numPinned : insertionPoint >= numPinned; + if (!ok) { + continue; + } + + indexMap.set(window, insertionPoint + 1); + + if (tab.ownerDocument.defaultView !== window) { // If the window we are moving the tab in is different, then move the tab // to the new window. - let newTab = gBrowser.addTab("about:blank"); - let newBrowser = gBrowser.getBrowserForTab(newTab); - gBrowser.updateBrowserRemotenessByURL(newBrowser, tab.linkedBrowser.currentURI.spec); - newBrowser.stop(); - // This is necessary for getter side-effects. - void newBrowser.docShell; - - if (tab.pinned) { - gBrowser.pinTab(newTab); - } - - gBrowser.moveTabTo(newTab, getInsertionPoint()); - - tab.parentNode._finishAnimateTabMove(); - gBrowser.swapBrowsersAndCloseOther(newTab, tab); - tab = newTab; + tab = gBrowser.adoptTab(tab, insertionPoint, false); } else { // If the window we are moving is the same, just move the tab. - gBrowser.moveTabTo(tab, getInsertionPoint()); + gBrowser.moveTabTo(tab, insertionPoint); } tabsMoved.push(tab); } diff --git a/browser/components/extensions/jar.mn b/browser/components/extensions/jar.mn index faa5d5891a0..0d800edeee9 100644 --- a/browser/components/extensions/jar.mn +++ b/browser/components/extensions/jar.mn @@ -8,6 +8,7 @@ browser.jar: content/browser/ext-contextMenus.js content/browser/ext-browserAction.js content/browser/ext-pageAction.js + content/browser/ext-desktop-runtime.js content/browser/ext-tabs.js content/browser/ext-windows.js content/browser/ext-bookmarks.js diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini index 0334eb42361..8db28d4acfe 100644 --- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -21,6 +21,7 @@ support-files = [browser_ext_contextMenus.js] [browser_ext_getViews.js] [browser_ext_lastError.js] +[browser_ext_runtime_setUninstallURL.js] [browser_ext_tabs_audio.js] [browser_ext_tabs_captureVisibleTab.js] [browser_ext_tabs_executeScript.js] diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js index a7c95edfd08..53fefefaa30 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js @@ -233,39 +233,43 @@ add_task(function* testInvalidIconSizes() { browser.tabs.query({ active: true, currentWindow: true }, tabs => { let tabId = tabs[0].id; + let promises = []; for (let api of ["pageAction", "browserAction"]) { // helper function to run setIcon and check if it fails let assertSetIconThrows = function(detail, error, message) { - try { - detail.tabId = tabId; - browser[api].setIcon(detail); - - browser.test.fail("Expected an error on invalid icon size."); - browser.test.notifyFail("setIcon with invalid icon size"); - return; - } catch (e) { - browser.test.succeed("setIcon with invalid icon size"); - } + detail.tabId = tabId; + promises.push( + browser[api].setIcon(detail).then( + () => { + browser.test.fail("Expected an error on invalid icon size."); + browser.test.notifyFail("setIcon with invalid icon size"); + }, + error => { + browser.test.succeed("setIcon with invalid icon size"); + })); }; + let imageData = new ImageData(1, 1); + // test invalid icon size inputs for (let type of ["path", "imageData"]) { - assertSetIconThrows({ [type]: { "abcdef": "test.png" } }); - assertSetIconThrows({ [type]: { "48px": "test.png" } }); - assertSetIconThrows({ [type]: { "20.5": "test.png" } }); - assertSetIconThrows({ [type]: { "5.0": "test.png" } }); - assertSetIconThrows({ [type]: { "-300": "test.png" } }); - assertSetIconThrows({ [type]: { - "abc": "test.png", - "5": "test.png" - }}); + let img = type == "imageData" ? imageData : "test.png"; + + assertSetIconThrows({ [type]: { "abcdef": img } }); + assertSetIconThrows({ [type]: { "48px": img } }); + assertSetIconThrows({ [type]: { "20.5": img } }); + assertSetIconThrows({ [type]: { "5.0": img } }); + assertSetIconThrows({ [type]: { "-300": img } }); + assertSetIconThrows({ [type]: { "abc": img, "5": img }}); } - assertSetIconThrows({ imageData: { "abcdef": "test.png" }, path: {"5": "test.png"} }); - assertSetIconThrows({ path: { "abcdef": "test.png" }, imageData: {"5": "test.png"} }); + assertSetIconThrows({ imageData: { "abcdef": imageData }, path: {"5": "test.png"} }); + assertSetIconThrows({ path: { "abcdef": "test.png" }, imageData: {"5": imageData} }); } - browser.test.notifyPass("setIcon with invalid icon size"); + Promise.all(promises).then(() => { + browser.test.notifyPass("setIcon with invalid icon size"); + }); }); } }); @@ -347,25 +351,24 @@ add_task(function* testSecureURLsDenied() { let urls = ["chrome://browser/content/browser.xul", "javascript:true"]; + let promises = []; for (let url of urls) { for (let api of ["pageAction", "browserAction"]) { - try { - browser[api].setIcon({tabId, path: url}); - - browser.test.fail(`Load of '${url}' succeeded. Expected failure.`); - browser.test.notifyFail("setIcon security tests"); - return; - } catch (e) { - // We can't actually inspect the error here, since the - // error object belongs to the privileged scope of the API, - // rather than to the extension scope that calls into it. - // Just assume it's the expected security error, for now. - browser.test.succeed(`Load of '${url}' failed. Expected failure.`); - } + promises.push( + browser[api].setIcon({tabId, path: url}).then( + () => { + browser.test.fail(`Load of '${url}' succeeded. Expected failure.`); + browser.test.notifyFail("setIcon security tests"); + }, + error => { + browser.test.succeed(`Load of '${url}' failed. Expected failure. ${error}`); + })); } } - browser.test.notifyPass("setIcon security tests"); + Promise.all(promises).then(() => { + browser.test.notifyPass("setIcon security tests"); + }); }); }, }); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus.js b/browser/components/extensions/test/browser/browser_ext_contextMenus.js index 3f8349bc3a7..bbaf38045dc 100644 --- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js @@ -54,12 +54,13 @@ add_task(function* () { { title: "child2", parentId: parentToDel, onclick: genericOnClick }); browser.contextMenus.remove(parentToDel); - try { - browser.contextMenus.update(parent, { parentId: child2 }); - browser.test.notifyFail(); - } catch (e) { - browser.test.notifyPass(); - } + browser.contextMenus.update(parent, { parentId: child2 }).then( + () => { + browser.test.notifyFail(); + }, + () => { + browser.test.notifyPass(); + }); }, }); diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js new file mode 100644 index 00000000000..e13a8d06d3c --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js @@ -0,0 +1,114 @@ +"use strict"; + +let { AddonManager } = Components.utils.import("resource://gre/modules/AddonManager.jsm", {}); +let { Extension } = Components.utils.import("resource://gre/modules/Extension.jsm", {}); + +function install(url) { + return new Promise((resolve, reject) => { + AddonManager.getInstallForURL(url, (install) => { + install.addListener({ + onInstallEnded: (i, addon) => resolve(addon), + onInstallFailed: () => reject(), + }); + install.install(); + }, "application/x-xpinstall"); + }); +} + +function* makeAndInstallXPI(id, backgroundScript, loadedURL) { + let xpi = Extension.generateXPI(id, { + background: "(" + backgroundScript.toString() + ")()", + }); + SimpleTest.registerCleanupFunction(function cleanupXPI() { + Services.obs.notifyObservers(xpi, "flush-cache-entry", null); + xpi.remove(false); + }); + + let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, loadedURL); + + let fileURI = Services.io.newFileURI(xpi); + info(`installing ${fileURI.spec}`); + let addon = yield install(fileURI.spec); + info("installed"); + + // A WebExtension is started asynchronously, we have our test extension + // open a new tab to signal that the background script has executed. + let loadTab = yield loadPromise; + yield BrowserTestUtils.removeTab(loadTab); + + return addon; +} + + +add_task(function* test_setuninstallurl_badargs() { + function backgroundScript() { + let promises = [ + browser.runtime.setUninstallURL("this is not a url") + .then(() => { + browser.test.notifyFail("setUninstallURL should have failed with bad url"); + }) + .catch(error => { + browser.test.assertTrue(/Invalid URL/.test(error.message), "error message indicates malformed url"); + }), + + browser.runtime.setUninstallURL("file:///etc/passwd") + .then(() => { + browser.test.notifyFail("setUninstallURL should have failed with non-http[s] url"); + }) + .catch(error => { + browser.test.assertTrue(/must have the scheme http or https/.test(error.message), "error message indicates bad scheme"); + }), + ]; + + Promise.all(promises) + .then(() => browser.test.notifyPass("setUninstallURL bad params")); + } + + let extension = ExtensionTestUtils.loadExtension({ + background: "(" + backgroundScript.toString() + ")()", + }); + yield extension.startup(); + yield extension.awaitFinish(); + yield extension.unload(); +}); + +// Test the documented behavior of setUninstallURL() that passing an +// empty string is equivalent to not setting an uninstall URL +// (i.e., no new tab is opened upon uninstall) +add_task(function* test_setuninstall_empty_url() { + function backgroundScript() { + browser.runtime.setUninstallURL("") + .then(() => browser.tabs.create({ url: "http://example.com/addon_loaded" })); + } + + let addon = yield makeAndInstallXPI("test_uinstallurl2@tests.mozilla.org", + backgroundScript, + "http://example.com/addon_loaded"); + + addon.uninstall(true); + info("uninstalled"); + + // no need to explicitly check for the absence of a new tab, + // BrowserTestUtils will eventually complain if one is opened. +}); + +add_task(function* test_setuninstallurl() { + function backgroundScript() { + browser.runtime.setUninstallURL("http://example.com/addon_uninstalled") + .then(() => browser.tabs.create({ url: "http://example.com/addon_loaded" })); + } + + let addon = yield makeAndInstallXPI("test_uinstallurl@tests.mozilla.org", + backgroundScript, + "http://example.com/addon_loaded"); + + // look for a new tab with the uninstall url. + let uninstallPromise = BrowserTestUtils.waitForNewTab(gBrowser, "http://example.com/addon_uninstalled"); + + addon.uninstall(true); + info("uninstalled"); + + let uninstalledTab = yield uninstallPromise; + isnot(uninstalledTab, null, "opened tab with uninstall url"); + yield BrowserTestUtils.removeTab(uninstalledTab); +}); diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index d724b0fd000..76a582eed4d 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -551,6 +551,7 @@ BrowserGlue.prototype = { ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js"); ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js"); ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js"); + ExtensionManagement.registerScript("chrome://browser/content/ext-desktop-runtime.js"); ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js"); ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js"); ExtensionManagement.registerScript("chrome://browser/content/ext-bookmarks.js"); diff --git a/browser/components/preferences/in-content/main.js b/browser/components/preferences/in-content/main.js index 249dc229c00..7f109d5154b 100644 --- a/browser/components/preferences/in-content/main.js +++ b/browser/components/preferences/in-content/main.js @@ -30,9 +30,9 @@ var gMainPane = { // In Windows 8 we launch the control panel since it's the only // way to get all file type association prefs. So we don't know // when the user will select the default. We refresh here periodically - // in case the default changes. On other Windows OS's defaults can also + // in case the default changes. On other Windows OS's defaults can also // be set while the prefs are open. - window.setInterval(this.updateSetDefaultBrowser, 1000); + window.setInterval(this.updateSetDefaultBrowser.bind(this), 1000); #endif #endif @@ -695,8 +695,11 @@ var gMainPane = { return; } let setDefaultPane = document.getElementById("setDefaultPane"); - let selectedIndex = shellSvc.isDefaultBrowser(false, true) ? 1 : 0; - setDefaultPane.selectedIndex = selectedIndex; + let isDefault = shellSvc.isDefaultBrowser(false, true); + setDefaultPane.selectedIndex = isDefault ? 1 : 0; + let alwaysCheck = document.getElementById("alwaysCheckDefault"); + alwaysCheck.disabled = alwaysCheck.disabled || + isDefault && alwaysCheck.checked; }, /** @@ -704,6 +707,9 @@ var gMainPane = { */ setDefaultBrowser: function() { + let alwaysCheckPref = document.getElementById("browser.shell.checkDefaultBrowser"); + alwaysCheckPref.value = true; + let shellSvc = getShellService(); if (!shellSvc) return; @@ -713,8 +719,8 @@ var gMainPane = { Cu.reportError(ex); return; } - let selectedIndex = - shellSvc.isDefaultBrowser(false, true) ? 1 : 0; + + let selectedIndex = shellSvc.isDefaultBrowser(false, true) ? 1 : 0; document.getElementById("setDefaultPane").selectedIndex = selectedIndex; } #endif diff --git a/browser/components/preferences/in-content/tests/browser.ini b/browser/components/preferences/in-content/tests/browser.ini index 2c54a9f5ae1..f98ee4bbc2f 100644 --- a/browser/components/preferences/in-content/tests/browser.ini +++ b/browser/components/preferences/in-content/tests/browser.ini @@ -17,6 +17,7 @@ skip-if = os != "win" # This test tests the windows-specific app selection dialo [browser_connection.js] [browser_connection_bug388287.js] [browser_cookies_exceptions.js] +[browser_defaultbrowser_alwayscheck.js] [browser_healthreport.js] skip-if = true || !healthreport # Bug 1185403 for the "true" [browser_homepages_filter_aboutpreferences.js] diff --git a/browser/components/preferences/in-content/tests/browser_defaultbrowser_alwayscheck.js b/browser/components/preferences/in-content/tests/browser_defaultbrowser_alwayscheck.js new file mode 100644 index 00000000000..bb67fd73e15 --- /dev/null +++ b/browser/components/preferences/in-content/tests/browser_defaultbrowser_alwayscheck.js @@ -0,0 +1,103 @@ +"use strict"; + +const CHECK_DEFAULT_INITIAL = Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"); + +add_task(function* clicking_make_default_checks_alwaysCheck_checkbox() { + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + + yield test_with_mock_shellservice({isDefault: false}, function*() { + let setDefaultPane = content.document.getElementById("setDefaultPane"); + is(setDefaultPane.selectedIndex, "0", + "The 'make default' pane should be visible when not default"); + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + is(alwaysCheck.checked, false, "Always Check is unchecked by default"); + is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), false, + "alwaysCheck pref should be false by default in test runs"); + + let setDefaultButton = content.document.getElementById("setDefaultButton"); + setDefaultButton.click(); + content.window.gMainPane.updateSetDefaultBrowser(); + + yield ContentTaskUtils.waitForCondition(() => alwaysCheck.checked, + "'Always Check' checkbox should get checked after clicking the 'Set Default' button"); + + is(alwaysCheck.checked, true, + "Clicking 'Make Default' checks the 'Always Check' checkbox"); + is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), true, + "Checking the checkbox should set the pref to true"); + is(alwaysCheck.disabled, true, + "'Always Check' checkbox is locked with default browser and alwaysCheck=true"); + is(setDefaultPane.selectedIndex, "1", + "The 'make default' pane should not be visible when default"); + is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), true, + "checkDefaultBrowser pref is now enabled"); + }); + + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser"); +}); + +add_task(function* clicking_make_default_checks_alwaysCheck_checkbox() { + Services.prefs.lockPref("browser.shell.checkDefaultBrowser"); + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences"); + + yield test_with_mock_shellservice({isDefault: false}, function*() { + let setDefaultPane = content.document.getElementById("setDefaultPane"); + is(setDefaultPane.selectedIndex, "0", + "The 'make default' pane should be visible when not default"); + let alwaysCheck = content.document.getElementById("alwaysCheckDefault"); + is(alwaysCheck.disabled, true, "Always Check is disabled when locked"); + is(alwaysCheck.checked, true, + "Always Check is checked because defaultPref is true and pref is locked"); + is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), true, + "alwaysCheck pref should ship with 'true' by default"); + + let setDefaultButton = content.document.getElementById("setDefaultButton"); + setDefaultButton.click(); + content.window.gMainPane.updateSetDefaultBrowser(); + + yield ContentTaskUtils.waitForCondition(() => setDefaultPane.selectedIndex == "1", + "Browser is now default"); + + is(alwaysCheck.checked, true, + "'Always Check' is still checked because it's locked"); + is(alwaysCheck.disabled, true, + "'Always Check is disabled because it's locked"); + is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), true, + "The pref is locked and so doesn't get changed"); + }); + + Services.prefs.unlockPref("browser.shell.checkDefaultBrowser"); + gBrowser.removeCurrentTab(); +}); + +registerCleanupFunction(function() { + Services.prefs.unlockPref("browser.shell.checkDefaultBrowser"); + Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", CHECK_DEFAULT_INITIAL); +}); + +function* test_with_mock_shellservice(options, testFn) { + yield ContentTask.spawn(gBrowser.selectedBrowser, options, function*(options) { + let doc = content.document; + let win = doc.defaultView; + win.oldShellService = win.getShellService(); + let mockShellService = { + _isDefault: false, + isDefaultBrowser() { + return this._isDefault; + }, + setDefaultBrowser() { + this._isDefault = true; + }, + }; + win.getShellService = function() { + return mockShellService; + } + mockShellService._isDefault = options.isDefault; + win.gMainPane.updateSetDefaultBrowser(); + }); + + yield ContentTask.spawn(gBrowser.selectedBrowser, null, testFn); + + Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", CHECK_DEFAULT_INITIAL); +} diff --git a/browser/extensions/pocket/bootstrap.js b/browser/extensions/pocket/bootstrap.js index f0d503e249d..8e58cab7c17 100644 --- a/browser/extensions/pocket/bootstrap.js +++ b/browser/extensions/pocket/bootstrap.js @@ -468,18 +468,6 @@ var PocketOverlay = { this.updatePocketItemVisibility(win.document); } }, - onWidgetReset: function(aNode, aContainer) { - // CUI was reset and doesn't respect default area for API widgets, place our - // widget back to the default area - // initially place the button after the bookmarks button if it is in the UI - let widgets = CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR); - let bmbtn = widgets.indexOf("bookmarks-menu-button"); - if (bmbtn > -1) { - CustomizableUI.addWidgetToArea("pocket-button", CustomizableUI.AREA_NAVBAR, bmbtn + 1); - } else { - CustomizableUI.addWidgetToArea("pocket-button", CustomizableUI.AREA_NAVBAR); - } - }, updatePocketItemVisibility: function(doc) { let hidden = !CustomizableUI.getPlacementOfWidget("pocket-button"); for (let prefix of ["panelMenu_", "menu_", "BMB_"]) { diff --git a/devtools/client/framework/devtools-browser.js b/devtools/client/framework/devtools-browser.js new file mode 100644 index 00000000000..73db765d04d --- /dev/null +++ b/devtools/client/framework/devtools-browser.js @@ -0,0 +1,828 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {Cc, Ci, Cu} = require("chrome"); +const Services = require("Services"); +const promise = require("promise"); +const {gDevTools} = require("./devtools"); + +// Load target and toolbox lazily as they need gDevTools to be fully initialized +loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true); +loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true); +loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true); +loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", + "resource:///modules/CustomizableUI.jsm"); + +const bundle = Services.strings.createBundle("chrome://devtools/locale/toolbox.properties"); + +/** + * gDevToolsBrowser exposes functions to connect the gDevTools instance with a + * Firefox instance. + */ +var gDevToolsBrowser = exports.gDevToolsBrowser = { + /** + * A record of the windows whose menus we altered, so we can undo the changes + * as the window is closed + */ + _trackedBrowserWindows: new Set(), + + _tabStats: { + peakOpen: 0, + peakPinned: 0, + histOpen: [], + histPinned: [] + }, + + /** + * This function is for the benefit of Tools:DevToolbox in + * browser/base/content/browser-sets.inc and should not be used outside + * of there + */ + // used by browser-sets.inc, command + toggleToolboxCommand: function(gBrowser) { + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + + // If a toolbox exists, using toggle from the Main window : + // - should close a docked toolbox + // - should focus a windowed toolbox + let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW; + isDocked ? toolbox.destroy() : gDevTools.showToolbox(target); + }, + + /** + * This function ensures the right commands are enabled in a window, + * depending on their relevant prefs. It gets run when a window is registered, + * or when any of the devtools prefs change. + */ + updateCommandAvailability: function(win) { + let doc = win.document; + + function toggleCmd(id, isEnabled) { + let cmd = doc.getElementById(id); + if (isEnabled) { + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } else { + cmd.setAttribute("disabled", "true"); + cmd.setAttribute("hidden", "true"); + } + }; + + // Enable developer toolbar? + let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled"); + toggleCmd("Tools:DevToolbar", devToolbarEnabled); + let focusEl = doc.getElementById("Tools:DevToolbarFocus"); + if (devToolbarEnabled) { + focusEl.removeAttribute("disabled"); + } else { + focusEl.setAttribute("disabled", "true"); + } + if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) { + win.DeveloperToolbar.show(false).catch(console.error); + } + + // Enable WebIDE? + let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled"); + toggleCmd("Tools:WebIDE", webIDEEnabled); + + let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled"); + if (webIDEEnabled && showWebIDEWidget) { + gDevToolsBrowser.installWebIDEWidget(); + } else { + gDevToolsBrowser.uninstallWebIDEWidget(); + } + + // Enable Browser Toolbox? + let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled"); + let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled"); + let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled; + toggleCmd("Tools:BrowserToolbox", remoteEnabled); + toggleCmd("Tools:BrowserContentToolbox", remoteEnabled && win.gMultiProcessBrowser); + + // Enable Error Console? + let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled"); + toggleCmd("Tools:ErrorConsole", consoleEnabled); + + // Enable DevTools connection screen, if the preference allows this. + toggleCmd("Tools:DevToolsConnect", devtoolsRemoteEnabled); + }, + + observe: function(subject, topic, prefName) { + if (prefName.endsWith("enabled")) { + for (let win of this._trackedBrowserWindows) { + this.updateCommandAvailability(win); + } + } + }, + + _prefObserverRegistered: false, + + ensurePrefObserver: function() { + if (!this._prefObserverRegistered) { + this._prefObserverRegistered = true; + Services.prefs.addObserver("devtools.", this, false); + } + }, + + + /** + * This function is for the benefit of Tools:{toolId} commands, + * triggered from the WebDeveloper menu and keyboard shortcuts. + * + * selectToolCommand's behavior: + * - if the toolbox is closed, + * we open the toolbox and select the tool + * - if the toolbox is open, and the targeted tool is not selected, + * we select it + * - if the toolbox is open, and the targeted tool is selected, + * and the host is NOT a window, we close the toolbox + * - if the toolbox is open, and the targeted tool is selected, + * and the host is a window, we raise the toolbox window + */ + // Used when: - registering a new tool + // - new xul window, to add menu items + selectToolCommand: function(gBrowser, toolId) { + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + let toolDefinition = gDevTools.getToolDefinition(toolId); + + if (toolbox && + (toolbox.currentToolId == toolId || + (toolId == "webconsole" && toolbox.splitConsole))) + { + toolbox.fireCustomKey(toolId); + + if (toolDefinition.preventClosingOnKey || toolbox.hostType == Toolbox.HostType.WINDOW) { + toolbox.raise(); + } else { + toolbox.destroy(); + } + gDevTools.emit("select-tool-command", toolId); + } else { + gDevTools.showToolbox(target, toolId).then(() => { + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + + toolbox.fireCustomKey(toolId); + gDevTools.emit("select-tool-command", toolId); + }); + } + }, + + /** + * Open a tab to allow connects to a remote browser + */ + // Used by browser-sets.inc, command + openConnectScreen: function(gBrowser) { + gBrowser.selectedTab = gBrowser.addTab("chrome://devtools/content/framework/connect/connect.xhtml"); + }, + + /** + * Open WebIDE + */ + // Used by browser-sets.inc, command + // itself, webide widget + openWebIDE: function() { + let win = Services.wm.getMostRecentWindow("devtools:webide"); + if (win) { + win.focus(); + } else { + Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null); + } + }, + + _getContentProcessTarget: function () { + // Create a DebuggerServer in order to connect locally to it + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + DebuggerServer.allowChromeProcess = true; + + let transport = DebuggerServer.connectPipe(); + let client = new DebuggerClient(transport); + + let deferred = promise.defer(); + client.connect().then(() => { + client.mainRoot.listProcesses(response => { + // Do nothing if there is only one process, the parent process. + let contentProcesses = response.processes.filter(p => (!p.parent)); + if (contentProcesses.length < 1) { + let msg = bundle.GetStringFromName("toolbox.noContentProcess.message"); + Services.prompt.alert(null, "", msg); + deferred.reject("No content processes available."); + return; + } + // Otherwise, arbitrary connect to the unique content process. + client.getProcess(contentProcesses[0].id) + .then(response => { + let options = { + form: response.form, + client: client, + chrome: true, + isTabActor: false + }; + return TargetFactory.forRemoteTab(options); + }) + .then(target => { + // Ensure closing the connection in order to cleanup + // the debugger client and also the server created in the + // content process + target.on("close", () => { + client.close(); + }); + deferred.resolve(target); + }); + }); + }); + + return deferred.promise; + }, + + // Used by browser-sets.inc, command + openContentProcessToolbox: function () { + this._getContentProcessTarget() + .then(target => { + // Display a new toolbox, in a new window, with debugger by default + return gDevTools.showToolbox(target, "jsdebugger", + Toolbox.HostType.WINDOW); + }); + }, + + /** + * Install WebIDE widget + */ + // Used by itself + installWebIDEWidget: function() { + if (this.isWebIDEWidgetInstalled()) { + return; + } + + let defaultArea; + if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) { + defaultArea = CustomizableUI.AREA_NAVBAR; + } else { + defaultArea = CustomizableUI.AREA_PANEL; + } + + CustomizableUI.createWidget({ + id: "webide-button", + shortcutId: "key_webide", + label: "devtools-webide-button2.label", + tooltiptext: "devtools-webide-button2.tooltiptext", + defaultArea: defaultArea, + onCommand: function(aEvent) { + gDevToolsBrowser.openWebIDE(); + } + }); + }, + + isWebIDEWidgetInstalled: function() { + let widgetWrapper = CustomizableUI.getWidget("webide-button"); + return !!(widgetWrapper && widgetWrapper.provider == CustomizableUI.PROVIDER_API); + }, + + /** + * The deferred promise will be resolved by WebIDE's UI.init() + */ + isWebIDEInitialized: promise.defer(), + + /** + * Uninstall WebIDE widget + */ + uninstallWebIDEWidget: function() { + if (this.isWebIDEWidgetInstalled()) { + CustomizableUI.removeWidgetFromArea("webide-button"); + } + CustomizableUI.destroyWidget("webide-button"); + }, + + /** + * Move WebIDE widget to the navbar + */ + // Used by webide.js + moveWebIDEWidgetInNavbar: function() { + CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR); + }, + + /** + * Add this DevTools's presence to a browser window's document + * + * @param {XULDocument} doc + * The document to which menuitems and handlers are to be added + */ + // Used by browser.js + registerBrowserWindow: function DT_registerBrowserWindow(win) { + this.updateCommandAvailability(win); + this.ensurePrefObserver(); + gDevToolsBrowser._trackedBrowserWindows.add(win); + gDevToolsBrowser._addAllToolsToMenu(win.document); + + if (this._isFirebugInstalled()) { + let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox"); + broadcaster.removeAttribute("key"); + } + + let tabContainer = win.gBrowser.tabContainer; + tabContainer.addEventListener("TabSelect", this, false); + tabContainer.addEventListener("TabOpen", this, false); + tabContainer.addEventListener("TabClose", this, false); + tabContainer.addEventListener("TabPinned", this, false); + tabContainer.addEventListener("TabUnpinned", this, false); + }, + + /** + * Add a to . + * Appending a element is not always enough. The needs + * to be detached and reattached to make sure the is taken into + * account (see bug 832984). + * + * @param {XULDocument} doc + * The document to which keys are to be added + * @param {XULElement} or {DocumentFragment} keys + * Keys to add + */ + attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) { + let devtoolsKeyset = doc.getElementById("devtoolsKeyset"); + + if (!devtoolsKeyset) { + devtoolsKeyset = doc.createElement("keyset"); + devtoolsKeyset.setAttribute("id", "devtoolsKeyset"); + } + devtoolsKeyset.appendChild(keys); + let mainKeyset = doc.getElementById("mainKeyset"); + mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset); + }, + + /** + * Hook the JS debugger tool to the "Debug Script" button of the slow script + * dialog. + */ + setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() { + let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"] + .getService(Ci.nsISlowScriptDebug); + let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); + + function slowScriptDebugHandler(aTab, aCallback) { + let target = TargetFactory.forTab(aTab); + + gDevTools.showToolbox(target, "jsdebugger").then(toolbox => { + let threadClient = toolbox.getCurrentPanel().panelWin.gThreadClient; + + // Break in place, which means resuming the debuggee thread and pausing + // right before the next step happens. + switch (threadClient.state) { + case "paused": + // When the debugger is already paused. + threadClient.resumeThenPause(); + aCallback(); + break; + case "attached": + // When the debugger is already open. + threadClient.interrupt(() => { + threadClient.resumeThenPause(); + aCallback(); + }); + break; + case "resuming": + // The debugger is newly opened. + threadClient.addOneTimeListener("resumed", () => { + threadClient.interrupt(() => { + threadClient.resumeThenPause(); + aCallback(); + }); + }); + break; + default: + throw Error("invalid thread client state in slow script debug handler: " + + threadClient.state); + } + }); + } + + debugService.activationHandler = function(aWindow) { + let chromeWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + + let setupFinished = false; + slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab, + () => { setupFinished = true; }); + + // Don't return from the interrupt handler until the debugger is brought + // up; no reason to continue executing the slow script. + let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + utils.enterModalState(); + while (!setupFinished) { + tm.currentThread.processNextEvent(true); + } + utils.leaveModalState(); + }; + + debugService.remoteActivationHandler = function(aBrowser, aCallback) { + let chromeWindow = aBrowser.ownerDocument.defaultView; + let tab = chromeWindow.gBrowser.getTabForBrowser(aBrowser); + chromeWindow.gBrowser.selected = tab; + + function callback() { + aCallback.finishDebuggerStartup(); + } + + slowScriptDebugHandler(tab, callback); + }; + }, + + /** + * Unset the slow script debug handler. + */ + unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() { + let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"] + .getService(Ci.nsISlowScriptDebug); + debugService.activationHandler = undefined; + }, + + /** + * Detect the presence of a Firebug. + * + * @return promise + */ + _isFirebugInstalled: function DT_isFirebugInstalled() { + let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons"); + return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1; + }, + + /** + * Add the menuitem for a tool to all open browser windows. + * + * @param {object} toolDefinition + * properties of the tool to add + */ + _addToolToWindows: function DT_addToolToWindows(toolDefinition) { + // No menu item or global shortcut is required for options panel. + if (!toolDefinition.inMenu) { + return; + } + + // Skip if the tool is disabled. + try { + if (toolDefinition.visibilityswitch && + !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) { + return; + } + } catch(e) {} + + // We need to insert the new tool in the right place, which means knowing + // the tool that comes before the tool that we're trying to add + let allDefs = gDevTools.getToolDefinitionArray(); + let prevDef; + for (let def of allDefs) { + if (!def.inMenu) { + continue; + } + if (def === toolDefinition) { + break; + } + prevDef = def; + } + + for (let win of gDevToolsBrowser._trackedBrowserWindows) { + let doc = win.document; + let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc); + + doc.getElementById("mainCommandSet").appendChild(elements.cmd); + + if (elements.key) { + this.attachKeybindingsToBrowser(doc, elements.key); + } + + doc.getElementById("mainBroadcasterSet").appendChild(elements.bc); + + let amp = doc.getElementById("appmenu_webDeveloper_popup"); + if (amp) { + let ref; + + if (prevDef != null) { + let menuitem = doc.getElementById("appmenuitem_" + prevDef.id); + ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null; + } else { + ref = doc.getElementById("appmenu_devtools_separator"); + } + + if (ref) { + amp.insertBefore(elements.appmenuitem, ref); + } + } + + let ref; + + if (prevDef) { + let menuitem = doc.getElementById("menuitem_" + prevDef.id); + ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null; + } else { + ref = doc.getElementById("menu_devtools_separator"); + } + + if (ref) { + ref.parentNode.insertBefore(elements.menuitem, ref); + } + } + + if (toolDefinition.id === "jsdebugger") { + gDevToolsBrowser.setSlowScriptDebugHandler(); + } + }, + + /** + * Add all tools to the developer tools menu of a window. + * + * @param {XULDocument} doc + * The document to which the tool items are to be added. + */ + _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) { + let fragCommands = doc.createDocumentFragment(); + let fragKeys = doc.createDocumentFragment(); + let fragBroadcasters = doc.createDocumentFragment(); + let fragAppMenuItems = doc.createDocumentFragment(); + let fragMenuItems = doc.createDocumentFragment(); + + for (let toolDefinition of gDevTools.getToolDefinitionArray()) { + if (!toolDefinition.inMenu) { + continue; + } + + let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc); + + if (!elements) { + return; + } + + fragCommands.appendChild(elements.cmd); + if (elements.key) { + fragKeys.appendChild(elements.key); + } + fragBroadcasters.appendChild(elements.bc); + fragAppMenuItems.appendChild(elements.appmenuitem); + fragMenuItems.appendChild(elements.menuitem); + } + + let mcs = doc.getElementById("mainCommandSet"); + mcs.appendChild(fragCommands); + + this.attachKeybindingsToBrowser(doc, fragKeys); + + let mbs = doc.getElementById("mainBroadcasterSet"); + mbs.appendChild(fragBroadcasters); + + let amps = doc.getElementById("appmenu_devtools_separator"); + if (amps) { + amps.parentNode.insertBefore(fragAppMenuItems, amps); + } + + let mps = doc.getElementById("menu_devtools_separator"); + if (mps) { + mps.parentNode.insertBefore(fragMenuItems, mps); + } + }, + + /** + * Add a menu entry for a tool definition + * + * @param {string} toolDefinition + * Tool definition of the tool to add a menu entry. + * @param {XULDocument} doc + * The document to which the tool menu item is to be added. + */ + _createToolMenuElements: function DT_createToolMenuElements(toolDefinition, doc) { + let id = toolDefinition.id; + + // Prevent multiple entries for the same tool. + if (doc.getElementById("Tools:" + id)) { + return; + } + + let cmd = doc.createElement("command"); + cmd.id = "Tools:" + id; + cmd.setAttribute("oncommand", + 'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");'); + + let key = null; + if (toolDefinition.key) { + key = doc.createElement("key"); + key.id = "key_" + id; + + if (toolDefinition.key.startsWith("VK_")) { + key.setAttribute("keycode", toolDefinition.key); + } else { + key.setAttribute("key", toolDefinition.key); + } + + key.setAttribute("command", cmd.id); + key.setAttribute("modifiers", toolDefinition.modifiers); + } + + let bc = doc.createElement("broadcaster"); + bc.id = "devtoolsMenuBroadcaster_" + id; + bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label); + bc.setAttribute("command", cmd.id); + + if (key) { + bc.setAttribute("key", "key_" + id); + } + + let appmenuitem = doc.createElement("menuitem"); + appmenuitem.id = "appmenuitem_" + id; + appmenuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id); + + let menuitem = doc.createElement("menuitem"); + menuitem.id = "menuitem_" + id; + menuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id); + + if (toolDefinition.accesskey) { + menuitem.setAttribute("accesskey", toolDefinition.accesskey); + } + + return { + cmd: cmd, + key: key, + bc: bc, + appmenuitem: appmenuitem, + menuitem: menuitem + }; + }, + + hasToolboxOpened: function(win) { + let tab = win.gBrowser.selectedTab; + for (let [target, toolbox] of gDevTools._toolboxes) { + if (target.tab == tab) { + return true; + } + } + return false; + }, + + /** + * Update the "Toggle Tools" checkbox in the developer tools menu. This is + * called when a toolbox is created or destroyed. + */ + _updateMenuCheckbox: function DT_updateMenuCheckbox() { + for (let win of gDevToolsBrowser._trackedBrowserWindows) { + + let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win); + + let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox"); + if (hasToolbox) { + broadcaster.setAttribute("checked", "true"); + } else { + broadcaster.removeAttribute("checked"); + } + } + }, + + /** + * Remove the menuitem for a tool to all open browser windows. + * + * @param {string} toolId + * id of the tool to remove + */ + _removeToolFromWindows: function DT_removeToolFromWindows(toolId) { + for (let win of gDevToolsBrowser._trackedBrowserWindows) { + gDevToolsBrowser._removeToolFromMenu(toolId, win.document); + } + + if (toolId === "jsdebugger") { + gDevToolsBrowser.unsetSlowScriptDebugHandler(); + } + }, + + /** + * Remove a tool's menuitem from a window + * + * @param {string} toolId + * Id of the tool to add a menu entry for + * @param {XULDocument} doc + * The document to which the tool menu item is to be removed from + */ + _removeToolFromMenu: function DT_removeToolFromMenu(toolId, doc) { + let command = doc.getElementById("Tools:" + toolId); + if (command) { + command.parentNode.removeChild(command); + } + + let key = doc.getElementById("key_" + toolId); + if (key) { + key.parentNode.removeChild(key); + } + + let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId); + if (bc) { + bc.parentNode.removeChild(bc); + } + + let appmenuitem = doc.getElementById("appmenuitem_" + toolId); + if (appmenuitem) { + appmenuitem.parentNode.removeChild(appmenuitem); + } + + let menuitem = doc.getElementById("menuitem_" + toolId); + if (menuitem) { + menuitem.parentNode.removeChild(menuitem); + } + }, + + /** + * Called on browser unload to remove menu entries, toolboxes and event + * listeners from the closed browser window. + * + * @param {XULWindow} win + * The window containing the menu entry + */ + forgetBrowserWindow: function DT_forgetBrowserWindow(win) { + gDevToolsBrowser._trackedBrowserWindows.delete(win); + + // Destroy toolboxes for closed window + for (let [target, toolbox] of gDevTools._toolboxes) { + if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) { + toolbox.destroy(); + } + } + + let tabContainer = win.gBrowser.tabContainer; + tabContainer.removeEventListener("TabSelect", this, false); + tabContainer.removeEventListener("TabOpen", this, false); + tabContainer.removeEventListener("TabClose", this, false); + tabContainer.removeEventListener("TabPinned", this, false); + tabContainer.removeEventListener("TabUnpinned", this, false); + }, + + handleEvent: function(event) { + switch (event.type) { + case "TabOpen": + case "TabClose": + case "TabPinned": + case "TabUnpinned": + let open = 0; + let pinned = 0; + + for (let win of this._trackedBrowserWindows) { + let tabContainer = win.gBrowser.tabContainer; + let numPinnedTabs = win.gBrowser._numPinnedTabs || 0; + let numTabs = tabContainer.itemCount - numPinnedTabs; + + open += numTabs; + pinned += numPinnedTabs; + } + + this._tabStats.histOpen.push(open); + this._tabStats.histPinned.push(pinned); + this._tabStats.peakOpen = Math.max(open, this._tabStats.peakOpen); + this._tabStats.peakPinned = Math.max(pinned, this._tabStats.peakPinned); + break; + case "TabSelect": + gDevToolsBrowser._updateMenuCheckbox(); + } + }, + + /** + * All browser windows have been closed, tidy up remaining objects. + */ + destroy: function() { + Services.prefs.removeObserver("devtools.", gDevToolsBrowser); + Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application"); + }, +} + +gDevTools.on("tool-registered", function(ev, toolId) { + let toolDefinition = gDevTools._tools.get(toolId); + gDevToolsBrowser._addToolToWindows(toolDefinition); +}); + +gDevTools.on("tool-unregistered", function(ev, toolId) { + if (typeof toolId != "string") { + toolId = toolId.id; + } + gDevToolsBrowser._removeToolFromWindows(toolId); +}); + +gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox); +gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox); + +Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false); + +// Load the browser devtools main module as the loader's main module. +// This is done precisely here as main.js ends up dispatching the +// tool-registered events we are listening in this module. +loader.main("devtools/client/main"); + diff --git a/devtools/client/framework/devtools.js b/devtools/client/framework/devtools.js new file mode 100644 index 00000000000..6b381c7ee3a --- /dev/null +++ b/devtools/client/framework/devtools.js @@ -0,0 +1,511 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Services = require("Services"); +const promise = require("promise"); + +// Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized +loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true); +loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true); + +const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} = + require("devtools/client/definitions"); +const EventEmitter = require("devtools/shared/event-emitter"); +const Telemetry = require("devtools/client/shared/telemetry"); +const {JsonView} = require("devtools/client/jsonview/main"); + +const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR"; +const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR"; +const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR"; +const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR"; + +const FORBIDDEN_IDS = new Set(["toolbox", ""]); +const MAX_ORDINAL = 99; + +/** + * DevTools is a class that represents a set of developer tools, it holds a + * set of tools and keeps track of open toolboxes in the browser. + */ +this.DevTools = function DevTools() { + this._tools = new Map(); // Map + this._themes = new Map(); // Map + this._toolboxes = new Map(); // Map + this._telemetry = new Telemetry(); + + // destroy() is an observer's handler so we need to preserve context. + this.destroy = this.destroy.bind(this); + this._teardown = this._teardown.bind(this); + + // JSON Viewer for 'application/json' documents. + JsonView.initialize(); + + EventEmitter.decorate(this); + + Services.obs.addObserver(this._teardown, "devtools-unloaded", false); + Services.obs.addObserver(this.destroy, "quit-application", false); +}; + +DevTools.prototype = { + /** + * Register a new developer tool. + * + * A definition is a light object that holds different information about a + * developer tool. This object is not supposed to have any operational code. + * See it as a "manifest". + * The only actual code lives in the build() function, which will be used to + * start an instance of this tool. + * + * Each toolDefinition has the following properties: + * - id: Unique identifier for this tool (string|required) + * - visibilityswitch: Property name to allow us to hide this tool from the + * DevTools Toolbox. + * A falsy value indicates that it cannot be hidden. + * - icon: URL pointing to a graphic which will be used as the src for an + * 16x16 img tag (string|required) + * - invertIconForLightTheme: The icon can automatically have an inversion + * filter applied (default is false). All builtin tools are true, but + * addons may omit this to prevent unwanted changes to the `icon` + * image. filter: invert(1) is applied to the image (boolean|optional) + * - url: URL pointing to a XUL/XHTML document containing the user interface + * (string|required) + * - label: Localized name for the tool to be displayed to the user + * (string|required) + * - hideInOptions: Boolean indicating whether or not this tool should be + shown in toolbox options or not. Defaults to false. + * (boolean) + * - build: Function that takes an iframe, which has been populated with the + * markup from |url|, and also the toolbox containing the panel. + * And returns an instance of ToolPanel (function|required) + */ + registerTool: function DT_registerTool(toolDefinition) { + let toolId = toolDefinition.id; + + if (!toolId || FORBIDDEN_IDS.has(toolId)) { + throw new Error("Invalid definition.id"); + } + + // Make sure that additional tools will always be able to be hidden. + // When being called from main.js, defaultTools has not yet been exported. + // But, we can assume that in this case, it is a default tool. + if (DefaultTools && DefaultTools.indexOf(toolDefinition) == -1) { + toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled"; + } + + this._tools.set(toolId, toolDefinition); + + this.emit("tool-registered", toolId); + }, + + /** + * Removes all tools that match the given |toolId| + * Needed so that add-ons can remove themselves when they are deactivated + * + * @param {string|object} tool + * Definition or the id of the tool to unregister. Passing the + * tool id should be avoided as it is a temporary measure. + * @param {boolean} isQuitApplication + * true to indicate that the call is due to app quit, so we should not + * cause a cascade of costly events + */ + unregisterTool: function DT_unregisterTool(tool, isQuitApplication) { + let toolId = null; + if (typeof tool == "string") { + toolId = tool; + tool = this._tools.get(tool); + } + else { + toolId = tool.id; + } + this._tools.delete(toolId); + + if (!isQuitApplication) { + this.emit("tool-unregistered", tool); + } + }, + + /** + * Sorting function used for sorting tools based on their ordinals. + */ + ordinalSort: function DT_ordinalSort(d1, d2) { + let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL; + let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL; + return o1 - o2; + }, + + getDefaultTools: function DT_getDefaultTools() { + return DefaultTools.sort(this.ordinalSort); + }, + + getAdditionalTools: function DT_getAdditionalTools() { + let tools = []; + for (let [key, value] of this._tools) { + if (DefaultTools.indexOf(value) == -1) { + tools.push(value); + } + } + return tools.sort(this.ordinalSort); + }, + + /** + * Get a tool definition if it exists and is enabled. + * + * @param {string} toolId + * The id of the tool to show + * + * @return {ToolDefinition|null} tool + * The ToolDefinition for the id or null. + */ + getToolDefinition: function DT_getToolDefinition(toolId) { + let tool = this._tools.get(toolId); + if (!tool) { + return null; + } else if (!tool.visibilityswitch) { + return tool; + } + + let enabled; + try { + enabled = Services.prefs.getBoolPref(tool.visibilityswitch); + } catch (e) { + enabled = true; + } + + return enabled ? tool : null; + }, + + /** + * Allow ToolBoxes to get at the list of tools that they should populate + * themselves with. + * + * @return {Map} tools + * A map of the the tool definitions registered in this instance + */ + getToolDefinitionMap: function DT_getToolDefinitionMap() { + let tools = new Map(); + + for (let [id, definition] of this._tools) { + if (this.getToolDefinition(id)) { + tools.set(id, definition); + } + } + + return tools; + }, + + /** + * Tools have an inherent ordering that can't be represented in a Map so + * getToolDefinitionArray provides an alternative representation of the + * definitions sorted by ordinal value. + * + * @return {Array} tools + * A sorted array of the tool definitions registered in this instance + */ + getToolDefinitionArray: function DT_getToolDefinitionArray() { + let definitions = []; + + for (let [id, definition] of this._tools) { + if (this.getToolDefinition(id)) { + definitions.push(definition); + } + } + + return definitions.sort(this.ordinalSort); + }, + + /** + * Register a new theme for developer tools toolbox. + * + * A definition is a light object that holds various information about a + * theme. + * + * Each themeDefinition has the following properties: + * - id: Unique identifier for this theme (string|required) + * - label: Localized name for the theme to be displayed to the user + * (string|required) + * - stylesheets: Array of URLs pointing to a CSS document(s) containing + * the theme style rules (array|required) + * - classList: Array of class names identifying the theme within a document. + * These names are set to document element when applying + * the theme (array|required) + * - onApply: Function that is executed by the framework when the theme + * is applied. The function takes the current iframe window + * and the previous theme id as arguments (function) + * - onUnapply: Function that is executed by the framework when the theme + * is unapplied. The function takes the current iframe window + * and the new theme id as arguments (function) + */ + registerTheme: function DT_registerTheme(themeDefinition) { + let themeId = themeDefinition.id; + + if (!themeId) { + throw new Error("Invalid theme id"); + } + + if (this._themes.get(themeId)) { + throw new Error("Theme with the same id is already registered"); + } + + this._themes.set(themeId, themeDefinition); + + this.emit("theme-registered", themeId); + }, + + /** + * Removes an existing theme from the list of registered themes. + * Needed so that add-ons can remove themselves when they are deactivated + * + * @param {string|object} theme + * Definition or the id of the theme to unregister. + */ + unregisterTheme: function DT_unregisterTheme(theme) { + let themeId = null; + if (typeof theme == "string") { + themeId = theme; + theme = this._themes.get(theme); + } + else { + themeId = theme.id; + } + + let currTheme = Services.prefs.getCharPref("devtools.theme"); + + // Note that we can't check if `theme` is an item + // of `DefaultThemes` as we end up reloading definitions + // module and end up with different theme objects + let isCoreTheme = DefaultThemes.some(t => t.id === themeId); + + // Reset the theme if an extension theme that's currently applied + // is being removed. + // Ignore shutdown since addons get disabled during that time. + if (!Services.startup.shuttingDown && + !isCoreTheme && + theme.id == currTheme) { + Services.prefs.setCharPref("devtools.theme", "light"); + + let data = { + pref: "devtools.theme", + newValue: "light", + oldValue: currTheme + }; + + this.emit("pref-changed", data); + + this.emit("theme-unregistered", theme); + } + + this._themes.delete(themeId); + }, + + /** + * Get a theme definition if it exists. + * + * @param {string} themeId + * The id of the theme + * + * @return {ThemeDefinition|null} theme + * The ThemeDefinition for the id or null. + */ + getThemeDefinition: function DT_getThemeDefinition(themeId) { + let theme = this._themes.get(themeId); + if (!theme) { + return null; + } + return theme; + }, + + /** + * Get map of registered themes. + * + * @return {Map} themes + * A map of the the theme definitions registered in this instance + */ + getThemeDefinitionMap: function DT_getThemeDefinitionMap() { + let themes = new Map(); + + for (let [id, definition] of this._themes) { + if (this.getThemeDefinition(id)) { + themes.set(id, definition); + } + } + + return themes; + }, + + /** + * Get registered themes definitions sorted by ordinal value. + * + * @return {Array} themes + * A sorted array of the theme definitions registered in this instance + */ + getThemeDefinitionArray: function DT_getThemeDefinitionArray() { + let definitions = []; + + for (let [id, definition] of this._themes) { + if (this.getThemeDefinition(id)) { + definitions.push(definition); + } + } + + return definitions.sort(this.ordinalSort); + }, + + /** + * Show a Toolbox for a target (either by creating a new one, or if a toolbox + * already exists for the target, by bring to the front the existing one) + * If |toolId| is specified then the displayed toolbox will have the + * specified tool selected. + * If |hostType| is specified then the toolbox will be displayed using the + * specified HostType. + * + * @param {Target} target + * The target the toolbox will debug + * @param {string} toolId + * The id of the tool to show + * @param {Toolbox.HostType} hostType + * The type of host (bottom, window, side) + * @param {object} hostOptions + * Options for host specifically + * + * @return {Toolbox} toolbox + * The toolbox that was opened + */ + showToolbox: function(target, toolId, hostType, hostOptions) { + let deferred = promise.defer(); + + let toolbox = this._toolboxes.get(target); + if (toolbox) { + + let hostPromise = (hostType != null && toolbox.hostType != hostType) ? + toolbox.switchHost(hostType) : + promise.resolve(null); + + if (toolId != null && toolbox.currentToolId != toolId) { + hostPromise = hostPromise.then(function() { + return toolbox.selectTool(toolId); + }); + } + + return hostPromise.then(function() { + toolbox.raise(); + return toolbox; + }); + } + else { + // No toolbox for target, create one + toolbox = new Toolbox(target, toolId, hostType, hostOptions); + + this.emit("toolbox-created", toolbox); + + this._toolboxes.set(target, toolbox); + + toolbox.once("destroy", () => { + this.emit("toolbox-destroy", target); + }); + + toolbox.once("destroyed", () => { + this._toolboxes.delete(target); + this.emit("toolbox-destroyed", target); + }); + + // If toolId was passed in, it will already be selected before the + // open promise resolves. + toolbox.open().then(() => { + deferred.resolve(toolbox); + this.emit("toolbox-ready", toolbox); + }); + } + + return deferred.promise; + }, + + /** + * Return the toolbox for a given target. + * + * @param {object} target + * Target value e.g. the target that owns this toolbox + * + * @return {Toolbox} toolbox + * The toolbox that is debugging the given target + */ + getToolbox: function DT_getToolbox(target) { + return this._toolboxes.get(target); + }, + + /** + * Close the toolbox for a given target + * + * @return promise + * This promise will resolve to false if no toolbox was found + * associated to the target. true, if the toolbox was successfully + * closed. + */ + closeToolbox: function DT_closeToolbox(target) { + let toolbox = this._toolboxes.get(target); + if (toolbox == null) { + return promise.resolve(false); + } + return toolbox.destroy().then(() => true); + }, + + _pingTelemetry: function() { + let mean = function(arr) { + if (arr.length === 0) { + return 0; + } + + let total = arr.reduce((a, b) => a + b); + return Math.ceil(total / arr.length); + }; + + let tabStats = gDevToolsBrowser._tabStats; + this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen); + this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen)); + this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned); + this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned)); + }, + + /** + * Called to tear down a tools provider. + */ + _teardown: function DT_teardown() { + for (let [target, toolbox] of this._toolboxes) { + toolbox.destroy(); + } + }, + + /** + * All browser windows have been closed, tidy up remaining objects. + */ + destroy: function() { + Services.obs.removeObserver(this.destroy, "quit-application"); + Services.obs.removeObserver(this._teardown, "devtools-unloaded"); + + for (let [key, tool] of this.getToolDefinitionMap()) { + this.unregisterTool(key, true); + } + + JsonView.destroy(); + + this._pingTelemetry(); + this._telemetry = null; + + // Cleaning down the toolboxes: i.e. + // for (let [target, toolbox] of this._toolboxes) toolbox.destroy(); + // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow + }, + + /** + * Iterator that yields each of the toolboxes. + */ + *[Symbol.iterator]() { + for (let toolbox of this._toolboxes) { + yield toolbox; + } + } +}; + +exports.gDevTools = new DevTools(); + diff --git a/devtools/client/framework/gDevTools.jsm b/devtools/client/framework/gDevTools.jsm index 361e24b11d5..227fe332c5d 100644 --- a/devtools/client/framework/gDevTools.jsm +++ b/devtools/client/framework/gDevTools.jsm @@ -4,540 +4,47 @@ "use strict"; -this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser" ]; +/** + * This JSM is here to keep some compatibility with existing add-ons. + * Please now use the modules: + * - devtools/client/framework/devtools for gDevTools + * - devtools/client/framework/devtools-browser for gDevToolsBrowser + * + * We still do use gDevTools.jsm in our codebase, + * bug 1245462 is going to ensure we no longer do that. + */ + +this.EXPORTED_SYMBOLS = [ "gDevTools", "gDevToolsBrowser" ]; const { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -// Make most dependencies be reloadable so that the reload addon -// can update all of them while keeping gDevTools.jsm as-is -// Bug 1188405 is going to refactor this JSM into a commonjs module -// so that it can be reloaded as other modules. -let require, loader, promise, DefaultTools, DefaultThemes; -let loadDependencies = () => { - let l = Cu.import("resource://devtools/shared/Loader.jsm", {}); - require = l.require; - loader = l.loader; - promise = require("promise"); - // Load target and toolbox lazily as they need gDevTools to be fully initialized - loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true); - loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true); - - XPCOMUtils.defineLazyModuleGetter(this, "console", - "resource://gre/modules/Console.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", - "resource:///modules/CustomizableUI.jsm"); - loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true); - loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true); - - let d = require("devtools/client/definitions"); - DefaultTools = d.defaultTools; - DefaultThemes = d.defaultThemes; -}; -loadDependencies(); - -const EventEmitter = require("devtools/shared/event-emitter"); -const Telemetry = require("devtools/client/shared/telemetry"); -const {JsonView} = require("devtools/client/jsonview/main"); - -const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR"; -const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR"; -const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR"; -const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR"; - -const FORBIDDEN_IDS = new Set(["toolbox", ""]); -const MAX_ORDINAL = 99; - -const bundle = Services.strings.createBundle("chrome://devtools/locale/toolbox.properties"); +const { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {}); /** - * DevTools is a class that represents a set of developer tools, it holds a - * set of tools and keeps track of open toolboxes in the browser. + * Do not directly map to the commonjs modules so that callsites of + * gDevTools.jsm do not have to do anything to access to the very last version + * of the module. The `devtools` and `browser` getter are always going to + * retrieve the very last version of the modules. */ -this.DevTools = function DevTools() { - this._tools = new Map(); // Map - this._themes = new Map(); // Map - this._toolboxes = new Map(); // Map - this._telemetry = new Telemetry(); - - // destroy() is an observer's handler so we need to preserve context. - this.destroy = this.destroy.bind(this); - this._teardown = this._teardown.bind(this); - - // JSON Viewer for 'application/json' documents. - JsonView.initialize(); - - EventEmitter.decorate(this); - - Services.obs.addObserver(this._teardown, "devtools-unloaded", false); - Services.obs.addObserver(this.destroy, "quit-application", false); -}; - -DevTools.prototype = { - /** - * Register a new developer tool. - * - * A definition is a light object that holds different information about a - * developer tool. This object is not supposed to have any operational code. - * See it as a "manifest". - * The only actual code lives in the build() function, which will be used to - * start an instance of this tool. - * - * Each toolDefinition has the following properties: - * - id: Unique identifier for this tool (string|required) - * - visibilityswitch: Property name to allow us to hide this tool from the - * DevTools Toolbox. - * A falsy value indicates that it cannot be hidden. - * - icon: URL pointing to a graphic which will be used as the src for an - * 16x16 img tag (string|required) - * - invertIconForLightTheme: The icon can automatically have an inversion - * filter applied (default is false). All builtin tools are true, but - * addons may omit this to prevent unwanted changes to the `icon` - * image. filter: invert(1) is applied to the image (boolean|optional) - * - url: URL pointing to a XUL/XHTML document containing the user interface - * (string|required) - * - label: Localized name for the tool to be displayed to the user - * (string|required) - * - hideInOptions: Boolean indicating whether or not this tool should be - shown in toolbox options or not. Defaults to false. - * (boolean) - * - build: Function that takes an iframe, which has been populated with the - * markup from |url|, and also the toolbox containing the panel. - * And returns an instance of ToolPanel (function|required) - */ - registerTool: function DT_registerTool(toolDefinition) { - let toolId = toolDefinition.id; - - if (!toolId || FORBIDDEN_IDS.has(toolId)) { - throw new Error("Invalid definition.id"); - } - - // Make sure that additional tools will always be able to be hidden. - // When being called from main.js, defaultTools has not yet been exported. - // But, we can assume that in this case, it is a default tool. - if (DefaultTools && DefaultTools.indexOf(toolDefinition) == -1) { - toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled"; - } - - this._tools.set(toolId, toolDefinition); - - this.emit("tool-registered", toolId); - }, - - /** - * Removes all tools that match the given |toolId| - * Needed so that add-ons can remove themselves when they are deactivated - * - * @param {string|object} tool - * Definition or the id of the tool to unregister. Passing the - * tool id should be avoided as it is a temporary measure. - * @param {boolean} isQuitApplication - * true to indicate that the call is due to app quit, so we should not - * cause a cascade of costly events - */ - unregisterTool: function DT_unregisterTool(tool, isQuitApplication) { - let toolId = null; - if (typeof tool == "string") { - toolId = tool; - tool = this._tools.get(tool); - } - else { - toolId = tool.id; - } - this._tools.delete(toolId); - - if (!isQuitApplication) { - this.emit("tool-unregistered", tool); - } - }, - - /** - * Sorting function used for sorting tools based on their ordinals. - */ - ordinalSort: function DT_ordinalSort(d1, d2) { - let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL; - let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL; - return o1 - o2; - }, - - getDefaultTools: function DT_getDefaultTools() { - return DefaultTools.sort(this.ordinalSort); - }, - - getAdditionalTools: function DT_getAdditionalTools() { - let tools = []; - for (let [key, value] of this._tools) { - if (DefaultTools.indexOf(value) == -1) { - tools.push(value); - } - } - return tools.sort(this.ordinalSort); - }, - - /** - * Get a tool definition if it exists and is enabled. - * - * @param {string} toolId - * The id of the tool to show - * - * @return {ToolDefinition|null} tool - * The ToolDefinition for the id or null. - */ - getToolDefinition: function DT_getToolDefinition(toolId) { - let tool = this._tools.get(toolId); - if (!tool) { - return null; - } else if (!tool.visibilityswitch) { - return tool; - } - - let enabled; - try { - enabled = Services.prefs.getBoolPref(tool.visibilityswitch); - } catch (e) { - enabled = true; - } - - return enabled ? tool : null; - }, - - /** - * Allow ToolBoxes to get at the list of tools that they should populate - * themselves with. - * - * @return {Map} tools - * A map of the the tool definitions registered in this instance - */ - getToolDefinitionMap: function DT_getToolDefinitionMap() { - let tools = new Map(); - - for (let [id, definition] of this._tools) { - if (this.getToolDefinition(id)) { - tools.set(id, definition); - } - } - - return tools; - }, - - /** - * Tools have an inherent ordering that can't be represented in a Map so - * getToolDefinitionArray provides an alternative representation of the - * definitions sorted by ordinal value. - * - * @return {Array} tools - * A sorted array of the tool definitions registered in this instance - */ - getToolDefinitionArray: function DT_getToolDefinitionArray() { - let definitions = []; - - for (let [id, definition] of this._tools) { - if (this.getToolDefinition(id)) { - definitions.push(definition); - } - } - - return definitions.sort(this.ordinalSort); - }, - - /** - * Register a new theme for developer tools toolbox. - * - * A definition is a light object that holds various information about a - * theme. - * - * Each themeDefinition has the following properties: - * - id: Unique identifier for this theme (string|required) - * - label: Localized name for the theme to be displayed to the user - * (string|required) - * - stylesheets: Array of URLs pointing to a CSS document(s) containing - * the theme style rules (array|required) - * - classList: Array of class names identifying the theme within a document. - * These names are set to document element when applying - * the theme (array|required) - * - onApply: Function that is executed by the framework when the theme - * is applied. The function takes the current iframe window - * and the previous theme id as arguments (function) - * - onUnapply: Function that is executed by the framework when the theme - * is unapplied. The function takes the current iframe window - * and the new theme id as arguments (function) - */ - registerTheme: function DT_registerTheme(themeDefinition) { - let themeId = themeDefinition.id; - - if (!themeId) { - throw new Error("Invalid theme id"); - } - - if (this._themes.get(themeId)) { - throw new Error("Theme with the same id is already registered"); - } - - this._themes.set(themeId, themeDefinition); - - this.emit("theme-registered", themeId); - }, - - /** - * Removes an existing theme from the list of registered themes. - * Needed so that add-ons can remove themselves when they are deactivated - * - * @param {string|object} theme - * Definition or the id of the theme to unregister. - */ - unregisterTheme: function DT_unregisterTheme(theme) { - let themeId = null; - if (typeof theme == "string") { - themeId = theme; - theme = this._themes.get(theme); - } - else { - themeId = theme.id; - } - - let currTheme = Services.prefs.getCharPref("devtools.theme"); - - // Note that we can't check if `theme` is an item - // of `DefaultThemes` as we end up reloading definitions - // module and end up with different theme objects - let isCoreTheme = DefaultThemes.some(t => t.id === themeId); - - // Reset the theme if an extension theme that's currently applied - // is being removed. - // Ignore shutdown since addons get disabled during that time. - if (!Services.startup.shuttingDown && - !isCoreTheme && - theme.id == currTheme) { - Services.prefs.setCharPref("devtools.theme", "light"); - - let data = { - pref: "devtools.theme", - newValue: "light", - oldValue: currTheme - }; - - gDevTools.emit("pref-changed", data); - - this.emit("theme-unregistered", theme); - } - - this._themes.delete(themeId); - }, - - /** - * Get a theme definition if it exists. - * - * @param {string} themeId - * The id of the theme - * - * @return {ThemeDefinition|null} theme - * The ThemeDefinition for the id or null. - */ - getThemeDefinition: function DT_getThemeDefinition(themeId) { - let theme = this._themes.get(themeId); - if (!theme) { - return null; - } - return theme; - }, - - /** - * Get map of registered themes. - * - * @return {Map} themes - * A map of the the theme definitions registered in this instance - */ - getThemeDefinitionMap: function DT_getThemeDefinitionMap() { - let themes = new Map(); - - for (let [id, definition] of this._themes) { - if (this.getThemeDefinition(id)) { - themes.set(id, definition); - } - } - - return themes; - }, - - /** - * Get registered themes definitions sorted by ordinal value. - * - * @return {Array} themes - * A sorted array of the theme definitions registered in this instance - */ - getThemeDefinitionArray: function DT_getThemeDefinitionArray() { - let definitions = []; - - for (let [id, definition] of this._themes) { - if (this.getThemeDefinition(id)) { - definitions.push(definition); - } - } - - return definitions.sort(this.ordinalSort); - }, - - /** - * Show a Toolbox for a target (either by creating a new one, or if a toolbox - * already exists for the target, by bring to the front the existing one) - * If |toolId| is specified then the displayed toolbox will have the - * specified tool selected. - * If |hostType| is specified then the toolbox will be displayed using the - * specified HostType. - * - * @param {Target} target - * The target the toolbox will debug - * @param {string} toolId - * The id of the tool to show - * @param {Toolbox.HostType} hostType - * The type of host (bottom, window, side) - * @param {object} hostOptions - * Options for host specifically - * - * @return {Toolbox} toolbox - * The toolbox that was opened - */ - showToolbox: function(target, toolId, hostType, hostOptions) { - let deferred = promise.defer(); - - let toolbox = this._toolboxes.get(target); - if (toolbox) { - - let hostPromise = (hostType != null && toolbox.hostType != hostType) ? - toolbox.switchHost(hostType) : - promise.resolve(null); - - if (toolId != null && toolbox.currentToolId != toolId) { - hostPromise = hostPromise.then(function() { - return toolbox.selectTool(toolId); - }); - } - - return hostPromise.then(function() { - toolbox.raise(); - return toolbox; - }); - } - else { - // No toolbox for target, create one - toolbox = new Toolbox(target, toolId, hostType, hostOptions); - - this.emit("toolbox-created", toolbox); - - this._toolboxes.set(target, toolbox); - - toolbox.once("destroy", () => { - this.emit("toolbox-destroy", target); - }); - - toolbox.once("destroyed", () => { - this._toolboxes.delete(target); - this.emit("toolbox-destroyed", target); - }); - - // If toolId was passed in, it will already be selected before the - // open promise resolves. - toolbox.open().then(() => { - deferred.resolve(toolbox); - this.emit("toolbox-ready", toolbox); - }); - } - - return deferred.promise; - }, - - /** - * Return the toolbox for a given target. - * - * @param {object} target - * Target value e.g. the target that owns this toolbox - * - * @return {Toolbox} toolbox - * The toolbox that is debugging the given target - */ - getToolbox: function DT_getToolbox(target) { - return this._toolboxes.get(target); - }, - - /** - * Close the toolbox for a given target - * - * @return promise - * This promise will resolve to false if no toolbox was found - * associated to the target. true, if the toolbox was successfully - * closed. - */ - closeToolbox: function DT_closeToolbox(target) { - let toolbox = this._toolboxes.get(target); - if (toolbox == null) { - return promise.resolve(false); - } - return toolbox.destroy().then(() => true); - }, - - _pingTelemetry: function() { - let mean = function(arr) { - if (arr.length === 0) { - return 0; - } - - let total = arr.reduce((a, b) => a + b); - return Math.ceil(total / arr.length); - }; - - let tabStats = gDevToolsBrowser._tabStats; - this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen); - this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen)); - this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned); - this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned)); - }, - - /** - * Called to tear down a tools provider. - */ - _teardown: function DT_teardown() { - for (let [target, toolbox] of this._toolboxes) { - toolbox.destroy(); - } - }, - - /** - * All browser windows have been closed, tidy up remaining objects. - */ - destroy: function() { - Services.obs.removeObserver(this.destroy, "quit-application"); - Services.obs.removeObserver(this._teardown, "devtools-unloaded"); - - for (let [key, tool] of this.getToolDefinitionMap()) { - this.unregisterTool(key, true); - } - - JsonView.destroy(); - - this._pingTelemetry(); - this._telemetry = null; - - // Cleaning down the toolboxes: i.e. - // for (let [target, toolbox] of this._toolboxes) toolbox.destroy(); - // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow - }, - - // Force reloading dependencies if the loader happens to have reloaded - reload() { - loadDependencies(); - }, - - /** - * Iterator that yields each of the toolboxes. - */ - *[Symbol.iterator]() { - for (let toolbox of this._toolboxes) { - yield toolbox; - } +Object.defineProperty(this, "require", { + get() { + let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); + return require; } -}; +}); +Object.defineProperty(this, "devtools", { + get() { + return require("devtools/client/framework/devtools").gDevTools; + } +}); +Object.defineProperty(this, "browser", { + get() { + return require("devtools/client/framework/devtools-browser").gDevToolsBrowser; + } +}); /** * gDevTools is a singleton that controls the Firefox Developer Tools. @@ -545,799 +52,112 @@ DevTools.prototype = { * It is an instance of a DevTools class that holds a set of tools. It has the * same lifetime as the browser. */ -var gDevTools = new DevTools(); -this.gDevTools = gDevTools; +let gDevToolsMethods = [ + // Used by the reload addon. + // Force reloading dependencies if the loader happens to have reloaded. + "reload", + + // Used by: - b2g desktop.js + // - nsContextMenu + // - /devtools code + "showToolbox", + + // Used by Addon SDK and /devtools + "closeToolbox", + "getToolbox", + + // Used by Addon SDK, main.js and tests: + "registerTool", + "registerTheme", + "unregisterTool", + "unregisterTheme", + + // Used by main.js and test + "getToolDefinitionArray", + "getThemeDefinitionArray", + + // Used by theme-switching.js + "getThemeDefinition", + "emit", + + // Used by /devtools + "on", + "off", + "once", + + // Used by tests + "getToolDefinitionMap", + "getThemeDefinitionMap", + "getDefaultTools", + "getAdditionalTools", + "getToolDefinition", +]; +this.gDevTools = { + // Used by tests + get _toolboxes() { + return devtools._toolboxes; + }, + get _tools() { + return devtools._tools; + }, + *[Symbol.iterator]() { + for (let toolbox of this._toolboxes) { + yield toolbox; + } + } +}; +gDevToolsMethods.forEach(name => { + this.gDevTools[name] = (...args) => { + return devtools[name].apply(devtools, args); + }; +}); + /** * gDevToolsBrowser exposes functions to connect the gDevTools instance with a * Firefox instance. */ -var gDevToolsBrowser = { - /** - * A record of the windows whose menus we altered, so we can undo the changes - * as the window is closed - */ - _trackedBrowserWindows: new Set(), +let gDevToolsBrowserMethods = [ + // used by browser-sets.inc, command + "toggleToolboxCommand", - _tabStats: { - peakOpen: 0, - peakPinned: 0, - histOpen: [], - histPinned: [] + // Used by browser.js itself, by setting a oncommand string... + "selectToolCommand", + + // Used by browser-sets.inc, command + "openConnectScreen", + + // Used by browser-sets.inc, command + // itself, webide widget + "openWebIDE", + + // Used by browser-sets.inc, command + "openContentProcessToolbox", + + // Used by webide.js + "moveWebIDEWidgetInNavbar", + + // Used by browser.js + "registerBrowserWindow", + + // Used by reload addon + "hasToolboxOpened", + + // Used by browser.js + "forgetBrowserWindow" +]; +this.gDevToolsBrowser = { + // Used by webide.js + get isWebIDEInitialized() { + return browser.isWebIDEInitialized; }, - - /** - * This function is for the benefit of Tools:DevToolbox in - * browser/base/content/browser-sets.inc and should not be used outside - * of there - */ - toggleToolboxCommand: function(gBrowser) { - let target = TargetFactory.forTab(gBrowser.selectedTab); - let toolbox = gDevTools.getToolbox(target); - - // If a toolbox exists, using toggle from the Main window : - // - should close a docked toolbox - // - should focus a windowed toolbox - let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW; - isDocked ? toolbox.destroy() : gDevTools.showToolbox(target); - }, - - /** - * This function ensures the right commands are enabled in a window, - * depending on their relevant prefs. It gets run when a window is registered, - * or when any of the devtools prefs change. - */ - updateCommandAvailability: function(win) { - let doc = win.document; - - function toggleCmd(id, isEnabled) { - let cmd = doc.getElementById(id); - if (isEnabled) { - cmd.removeAttribute("disabled"); - cmd.removeAttribute("hidden"); - } else { - cmd.setAttribute("disabled", "true"); - cmd.setAttribute("hidden", "true"); - } - }; - - // Enable developer toolbar? - let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled"); - toggleCmd("Tools:DevToolbar", devToolbarEnabled); - let focusEl = doc.getElementById("Tools:DevToolbarFocus"); - if (devToolbarEnabled) { - focusEl.removeAttribute("disabled"); - } else { - focusEl.setAttribute("disabled", "true"); - } - if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) { - win.DeveloperToolbar.show(false).catch(console.error); - } - - // Enable WebIDE? - let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled"); - toggleCmd("Tools:WebIDE", webIDEEnabled); - - let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled"); - if (webIDEEnabled && showWebIDEWidget) { - gDevToolsBrowser.installWebIDEWidget(); - } else { - gDevToolsBrowser.uninstallWebIDEWidget(); - } - - // Enable Browser Toolbox? - let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled"); - let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled"); - let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled; - toggleCmd("Tools:BrowserToolbox", remoteEnabled); - toggleCmd("Tools:BrowserContentToolbox", remoteEnabled && win.gMultiProcessBrowser); - - // Enable Error Console? - let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled"); - toggleCmd("Tools:ErrorConsole", consoleEnabled); - - // Enable DevTools connection screen, if the preference allows this. - toggleCmd("Tools:DevToolsConnect", devtoolsRemoteEnabled); - }, - - observe: function(subject, topic, prefName) { - if (prefName.endsWith("enabled")) { - for (let win of this._trackedBrowserWindows) { - this.updateCommandAvailability(win); - } - } - }, - - _prefObserverRegistered: false, - - ensurePrefObserver: function() { - if (!this._prefObserverRegistered) { - this._prefObserverRegistered = true; - Services.prefs.addObserver("devtools.", this, false); - } - }, - - - /** - * This function is for the benefit of Tools:{toolId} commands, - * triggered from the WebDeveloper menu and keyboard shortcuts. - * - * selectToolCommand's behavior: - * - if the toolbox is closed, - * we open the toolbox and select the tool - * - if the toolbox is open, and the targeted tool is not selected, - * we select it - * - if the toolbox is open, and the targeted tool is selected, - * and the host is NOT a window, we close the toolbox - * - if the toolbox is open, and the targeted tool is selected, - * and the host is a window, we raise the toolbox window - */ - selectToolCommand: function(gBrowser, toolId) { - let target = TargetFactory.forTab(gBrowser.selectedTab); - let toolbox = gDevTools.getToolbox(target); - let toolDefinition = gDevTools.getToolDefinition(toolId); - - if (toolbox && - (toolbox.currentToolId == toolId || - (toolId == "webconsole" && toolbox.splitConsole))) - { - toolbox.fireCustomKey(toolId); - - if (toolDefinition.preventClosingOnKey || toolbox.hostType == Toolbox.HostType.WINDOW) { - toolbox.raise(); - } else { - toolbox.destroy(); - } - gDevTools.emit("select-tool-command", toolId); - } else { - gDevTools.showToolbox(target, toolId).then(() => { - let target = TargetFactory.forTab(gBrowser.selectedTab); - let toolbox = gDevTools.getToolbox(target); - - toolbox.fireCustomKey(toolId); - gDevTools.emit("select-tool-command", toolId); - }); - } - }, - - /** - * Open a tab to allow connects to a remote browser - */ - openConnectScreen: function(gBrowser) { - gBrowser.selectedTab = gBrowser.addTab("chrome://devtools/content/framework/connect/connect.xhtml"); - }, - - /** - * Open WebIDE - */ - openWebIDE: function() { - let win = Services.wm.getMostRecentWindow("devtools:webide"); - if (win) { - win.focus(); - } else { - Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null); - } - }, - - _getContentProcessTarget: function () { - // Create a DebuggerServer in order to connect locally to it - if (!DebuggerServer.initialized) { - DebuggerServer.init(); - DebuggerServer.addBrowserActors(); - } - DebuggerServer.allowChromeProcess = true; - - let transport = DebuggerServer.connectPipe(); - let client = new DebuggerClient(transport); - - let deferred = promise.defer(); - client.connect().then(() => { - client.mainRoot.listProcesses(response => { - // Do nothing if there is only one process, the parent process. - let contentProcesses = response.processes.filter(p => (!p.parent)); - if (contentProcesses.length < 1) { - let msg = bundle.GetStringFromName("toolbox.noContentProcess.message"); - Services.prompt.alert(null, "", msg); - deferred.reject("No content processes available."); - return; - } - // Otherwise, arbitrary connect to the unique content process. - client.getProcess(contentProcesses[0].id) - .then(response => { - let options = { - form: response.form, - client: client, - chrome: true, - isTabActor: false - }; - return TargetFactory.forRemoteTab(options); - }) - .then(target => { - // Ensure closing the connection in order to cleanup - // the debugger client and also the server created in the - // content process - target.on("close", () => { - client.close(); - }); - deferred.resolve(target); - }); - }); - }); - - return deferred.promise; - }, - - openContentProcessToolbox: function () { - this._getContentProcessTarget() - .then(target => { - // Display a new toolbox, in a new window, with debugger by default - return gDevTools.showToolbox(target, "jsdebugger", - Toolbox.HostType.WINDOW); - }); - }, - - /** - * Install WebIDE widget - */ - installWebIDEWidget: function() { - if (this.isWebIDEWidgetInstalled()) { - return; - } - - let defaultArea; - if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) { - defaultArea = CustomizableUI.AREA_NAVBAR; - } else { - defaultArea = CustomizableUI.AREA_PANEL; - } - - CustomizableUI.createWidget({ - id: "webide-button", - shortcutId: "key_webide", - label: "devtools-webide-button2.label", - tooltiptext: "devtools-webide-button2.tooltiptext", - defaultArea: defaultArea, - onCommand: function(aEvent) { - gDevToolsBrowser.openWebIDE(); - } - }); - }, - - isWebIDEWidgetInstalled: function() { - let widgetWrapper = CustomizableUI.getWidget("webide-button"); - return !!(widgetWrapper && widgetWrapper.provider == CustomizableUI.PROVIDER_API); - }, - - /** - * The deferred promise will be resolved by WebIDE's UI.init() - */ - isWebIDEInitialized: promise.defer(), - - /** - * Uninstall WebIDE widget - */ - uninstallWebIDEWidget: function() { - if (this.isWebIDEWidgetInstalled()) { - CustomizableUI.removeWidgetFromArea("webide-button"); - } - CustomizableUI.destroyWidget("webide-button"); - }, - - /** - * Move WebIDE widget to the navbar - */ - moveWebIDEWidgetInNavbar: function() { - CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR); - }, - - /** - * Add this DevTools's presence to a browser window's document - * - * @param {XULDocument} doc - * The document to which menuitems and handlers are to be added - */ - registerBrowserWindow: function DT_registerBrowserWindow(win) { - this.updateCommandAvailability(win); - this.ensurePrefObserver(); - gDevToolsBrowser._trackedBrowserWindows.add(win); - gDevToolsBrowser._addAllToolsToMenu(win.document); - - if (this._isFirebugInstalled()) { - let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox"); - broadcaster.removeAttribute("key"); - } - - let tabContainer = win.gBrowser.tabContainer; - tabContainer.addEventListener("TabSelect", this, false); - tabContainer.addEventListener("TabOpen", this, false); - tabContainer.addEventListener("TabClose", this, false); - tabContainer.addEventListener("TabPinned", this, false); - tabContainer.addEventListener("TabUnpinned", this, false); - }, - - /** - * Add a to . - * Appending a element is not always enough. The needs - * to be detached and reattached to make sure the is taken into - * account (see bug 832984). - * - * @param {XULDocument} doc - * The document to which keys are to be added - * @param {XULElement} or {DocumentFragment} keys - * Keys to add - */ - attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) { - let devtoolsKeyset = doc.getElementById("devtoolsKeyset"); - - if (!devtoolsKeyset) { - devtoolsKeyset = doc.createElement("keyset"); - devtoolsKeyset.setAttribute("id", "devtoolsKeyset"); - } - devtoolsKeyset.appendChild(keys); - let mainKeyset = doc.getElementById("mainKeyset"); - mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset); - }, - - /** - * Hook the JS debugger tool to the "Debug Script" button of the slow script - * dialog. - */ - setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() { - let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"] - .getService(Ci.nsISlowScriptDebug); - let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); - - function slowScriptDebugHandler(aTab, aCallback) { - let target = TargetFactory.forTab(aTab); - - gDevTools.showToolbox(target, "jsdebugger").then(toolbox => { - let threadClient = toolbox.getCurrentPanel().panelWin.gThreadClient; - - // Break in place, which means resuming the debuggee thread and pausing - // right before the next step happens. - switch (threadClient.state) { - case "paused": - // When the debugger is already paused. - threadClient.resumeThenPause(); - aCallback(); - break; - case "attached": - // When the debugger is already open. - threadClient.interrupt(() => { - threadClient.resumeThenPause(); - aCallback(); - }); - break; - case "resuming": - // The debugger is newly opened. - threadClient.addOneTimeListener("resumed", () => { - threadClient.interrupt(() => { - threadClient.resumeThenPause(); - aCallback(); - }); - }); - break; - default: - throw Error("invalid thread client state in slow script debug handler: " + - threadClient.state); - } - }); - } - - debugService.activationHandler = function(aWindow) { - let chromeWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem) - .rootTreeItem - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow) - .QueryInterface(Ci.nsIDOMChromeWindow); - - let setupFinished = false; - slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab, - () => { setupFinished = true; }); - - // Don't return from the interrupt handler until the debugger is brought - // up; no reason to continue executing the slow script. - let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - utils.enterModalState(); - while (!setupFinished) { - tm.currentThread.processNextEvent(true); - } - utils.leaveModalState(); - }; - - debugService.remoteActivationHandler = function(aBrowser, aCallback) { - let chromeWindow = aBrowser.ownerDocument.defaultView; - let tab = chromeWindow.gBrowser.getTabForBrowser(aBrowser); - chromeWindow.gBrowser.selected = tab; - - function callback() { - aCallback.finishDebuggerStartup(); - } - - slowScriptDebugHandler(tab, callback); - }; - }, - - /** - * Unset the slow script debug handler. - */ - unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() { - let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"] - .getService(Ci.nsISlowScriptDebug); - debugService.activationHandler = undefined; - }, - - /** - * Detect the presence of a Firebug. - * - * @return promise - */ - _isFirebugInstalled: function DT_isFirebugInstalled() { - let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons"); - return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1; - }, - - /** - * Add the menuitem for a tool to all open browser windows. - * - * @param {object} toolDefinition - * properties of the tool to add - */ - _addToolToWindows: function DT_addToolToWindows(toolDefinition) { - // No menu item or global shortcut is required for options panel. - if (!toolDefinition.inMenu) { - return; - } - - // Skip if the tool is disabled. - try { - if (toolDefinition.visibilityswitch && - !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) { - return; - } - } catch(e) {} - - // We need to insert the new tool in the right place, which means knowing - // the tool that comes before the tool that we're trying to add - let allDefs = gDevTools.getToolDefinitionArray(); - let prevDef; - for (let def of allDefs) { - if (!def.inMenu) { - continue; - } - if (def === toolDefinition) { - break; - } - prevDef = def; - } - - for (let win of gDevToolsBrowser._trackedBrowserWindows) { - let doc = win.document; - let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc); - - doc.getElementById("mainCommandSet").appendChild(elements.cmd); - - if (elements.key) { - this.attachKeybindingsToBrowser(doc, elements.key); - } - - doc.getElementById("mainBroadcasterSet").appendChild(elements.bc); - - let amp = doc.getElementById("appmenu_webDeveloper_popup"); - if (amp) { - let ref; - - if (prevDef != null) { - let menuitem = doc.getElementById("appmenuitem_" + prevDef.id); - ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null; - } else { - ref = doc.getElementById("appmenu_devtools_separator"); - } - - if (ref) { - amp.insertBefore(elements.appmenuitem, ref); - } - } - - let ref; - - if (prevDef) { - let menuitem = doc.getElementById("menuitem_" + prevDef.id); - ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null; - } else { - ref = doc.getElementById("menu_devtools_separator"); - } - - if (ref) { - ref.parentNode.insertBefore(elements.menuitem, ref); - } - } - - if (toolDefinition.id === "jsdebugger") { - gDevToolsBrowser.setSlowScriptDebugHandler(); - } - }, - - /** - * Add all tools to the developer tools menu of a window. - * - * @param {XULDocument} doc - * The document to which the tool items are to be added. - */ - _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) { - let fragCommands = doc.createDocumentFragment(); - let fragKeys = doc.createDocumentFragment(); - let fragBroadcasters = doc.createDocumentFragment(); - let fragAppMenuItems = doc.createDocumentFragment(); - let fragMenuItems = doc.createDocumentFragment(); - - for (let toolDefinition of gDevTools.getToolDefinitionArray()) { - if (!toolDefinition.inMenu) { - continue; - } - - let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc); - - if (!elements) { - return; - } - - fragCommands.appendChild(elements.cmd); - if (elements.key) { - fragKeys.appendChild(elements.key); - } - fragBroadcasters.appendChild(elements.bc); - fragAppMenuItems.appendChild(elements.appmenuitem); - fragMenuItems.appendChild(elements.menuitem); - } - - let mcs = doc.getElementById("mainCommandSet"); - mcs.appendChild(fragCommands); - - this.attachKeybindingsToBrowser(doc, fragKeys); - - let mbs = doc.getElementById("mainBroadcasterSet"); - mbs.appendChild(fragBroadcasters); - - let amps = doc.getElementById("appmenu_devtools_separator"); - if (amps) { - amps.parentNode.insertBefore(fragAppMenuItems, amps); - } - - let mps = doc.getElementById("menu_devtools_separator"); - if (mps) { - mps.parentNode.insertBefore(fragMenuItems, mps); - } - }, - - /** - * Add a menu entry for a tool definition - * - * @param {string} toolDefinition - * Tool definition of the tool to add a menu entry. - * @param {XULDocument} doc - * The document to which the tool menu item is to be added. - */ - _createToolMenuElements: function DT_createToolMenuElements(toolDefinition, doc) { - let id = toolDefinition.id; - - // Prevent multiple entries for the same tool. - if (doc.getElementById("Tools:" + id)) { - return; - } - - let cmd = doc.createElement("command"); - cmd.id = "Tools:" + id; - cmd.setAttribute("oncommand", - 'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");'); - - let key = null; - if (toolDefinition.key) { - key = doc.createElement("key"); - key.id = "key_" + id; - - if (toolDefinition.key.startsWith("VK_")) { - key.setAttribute("keycode", toolDefinition.key); - } else { - key.setAttribute("key", toolDefinition.key); - } - - key.setAttribute("command", cmd.id); - key.setAttribute("modifiers", toolDefinition.modifiers); - } - - let bc = doc.createElement("broadcaster"); - bc.id = "devtoolsMenuBroadcaster_" + id; - bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label); - bc.setAttribute("command", cmd.id); - - if (key) { - bc.setAttribute("key", "key_" + id); - } - - let appmenuitem = doc.createElement("menuitem"); - appmenuitem.id = "appmenuitem_" + id; - appmenuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id); - - let menuitem = doc.createElement("menuitem"); - menuitem.id = "menuitem_" + id; - menuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id); - - if (toolDefinition.accesskey) { - menuitem.setAttribute("accesskey", toolDefinition.accesskey); - } - - return { - cmd: cmd, - key: key, - bc: bc, - appmenuitem: appmenuitem, - menuitem: menuitem - }; - }, - - hasToolboxOpened: function(win) { - let tab = win.gBrowser.selectedTab; - for (let [target, toolbox] of gDevTools._toolboxes) { - if (target.tab == tab) { - return true; - } - } - return false; - }, - - /** - * Update the "Toggle Tools" checkbox in the developer tools menu. This is - * called when a toolbox is created or destroyed. - */ - _updateMenuCheckbox: function DT_updateMenuCheckbox() { - for (let win of gDevToolsBrowser._trackedBrowserWindows) { - - let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win); - - let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox"); - if (hasToolbox) { - broadcaster.setAttribute("checked", "true"); - } else { - broadcaster.removeAttribute("checked"); - } - } - }, - - /** - * Remove the menuitem for a tool to all open browser windows. - * - * @param {string} toolId - * id of the tool to remove - */ - _removeToolFromWindows: function DT_removeToolFromWindows(toolId) { - for (let win of gDevToolsBrowser._trackedBrowserWindows) { - gDevToolsBrowser._removeToolFromMenu(toolId, win.document); - } - - if (toolId === "jsdebugger") { - gDevToolsBrowser.unsetSlowScriptDebugHandler(); - } - }, - - /** - * Remove a tool's menuitem from a window - * - * @param {string} toolId - * Id of the tool to add a menu entry for - * @param {XULDocument} doc - * The document to which the tool menu item is to be removed from - */ - _removeToolFromMenu: function DT_removeToolFromMenu(toolId, doc) { - let command = doc.getElementById("Tools:" + toolId); - if (command) { - command.parentNode.removeChild(command); - } - - let key = doc.getElementById("key_" + toolId); - if (key) { - key.parentNode.removeChild(key); - } - - let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId); - if (bc) { - bc.parentNode.removeChild(bc); - } - - let appmenuitem = doc.getElementById("appmenuitem_" + toolId); - if (appmenuitem) { - appmenuitem.parentNode.removeChild(appmenuitem); - } - - let menuitem = doc.getElementById("menuitem_" + toolId); - if (menuitem) { - menuitem.parentNode.removeChild(menuitem); - } - }, - - /** - * Called on browser unload to remove menu entries, toolboxes and event - * listeners from the closed browser window. - * - * @param {XULWindow} win - * The window containing the menu entry - */ - forgetBrowserWindow: function DT_forgetBrowserWindow(win) { - gDevToolsBrowser._trackedBrowserWindows.delete(win); - - // Destroy toolboxes for closed window - for (let [target, toolbox] of gDevTools._toolboxes) { - if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) { - toolbox.destroy(); - } - } - - let tabContainer = win.gBrowser.tabContainer; - tabContainer.removeEventListener("TabSelect", this, false); - tabContainer.removeEventListener("TabOpen", this, false); - tabContainer.removeEventListener("TabClose", this, false); - tabContainer.removeEventListener("TabPinned", this, false); - tabContainer.removeEventListener("TabUnpinned", this, false); - }, - - handleEvent: function(event) { - switch (event.type) { - case "TabOpen": - case "TabClose": - case "TabPinned": - case "TabUnpinned": - let open = 0; - let pinned = 0; - - for (let win of this._trackedBrowserWindows) { - let tabContainer = win.gBrowser.tabContainer; - let numPinnedTabs = win.gBrowser._numPinnedTabs || 0; - let numTabs = tabContainer.itemCount - numPinnedTabs; - - open += numTabs; - pinned += numPinnedTabs; - } - - this._tabStats.histOpen.push(open); - this._tabStats.histPinned.push(pinned); - this._tabStats.peakOpen = Math.max(open, this._tabStats.peakOpen); - this._tabStats.peakPinned = Math.max(pinned, this._tabStats.peakPinned); - break; - case "TabSelect": - gDevToolsBrowser._updateMenuCheckbox(); - } - }, - - /** - * All browser windows have been closed, tidy up remaining objects. - */ - destroy: function() { - Services.prefs.removeObserver("devtools.", gDevToolsBrowser); - Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application"); - }, -} - -this.gDevToolsBrowser = gDevToolsBrowser; - -gDevTools.on("tool-registered", function(ev, toolId) { - let toolDefinition = gDevTools._tools.get(toolId); - gDevToolsBrowser._addToolToWindows(toolDefinition); -}); - -gDevTools.on("tool-unregistered", function(ev, toolId) { - if (typeof toolId != "string") { - toolId = toolId.id; + // Used by a test (should be removed) + get _trackedBrowserWindows() { + return browser._trackedBrowserWindows; } - gDevToolsBrowser._removeToolFromWindows(toolId); +}; +gDevToolsBrowserMethods.forEach(name => { + this.gDevToolsBrowser[name] = (...args) => { + return browser[name].apply(browser, args); + }; }); - -gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox); -gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox); - -Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false); - -// Load the browser devtools main module as the loader's main module. -loader.main("devtools/client/main"); diff --git a/devtools/client/framework/moz.build b/devtools/client/framework/moz.build index b86982d3d76..b5d645d2a18 100644 --- a/devtools/client/framework/moz.build +++ b/devtools/client/framework/moz.build @@ -11,6 +11,8 @@ TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [ DevToolsModules( 'attach-thread.js', + 'devtools-browser.js', + 'devtools.js', 'gDevTools.jsm', 'selection.js', 'sidebar.js', diff --git a/devtools/client/framework/toolbox-process-window.js b/devtools/client/framework/toolbox-process-window.js index 61378d65e1d..a24e4265370 100644 --- a/devtools/client/framework/toolbox-process-window.js +++ b/devtools/client/framework/toolbox-process-window.js @@ -5,8 +5,12 @@ var { classes: Cc, interfaces: Ci, utils: Cu } = Components; -var { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {}); -var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +// Require this module just to setup things like themes and tools +// devtools-browser is special as it loads main module +// To be cleaned up in bug 1247203. +require("devtools/client/framework/devtools-browser"); +var { gDevTools } = require("devtools/client/framework/devtools"); var { TargetFactory } = require("devtools/client/framework/target"); var { Toolbox } = require("devtools/client/framework/toolbox"); var { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); diff --git a/devtools/client/responsive.html/actions/index.js b/devtools/client/responsive.html/actions/index.js index ea7f6673302..9e84fa8f887 100644 --- a/devtools/client/responsive.html/actions/index.js +++ b/devtools/client/responsive.html/actions/index.js @@ -17,6 +17,9 @@ createEnum([ // Add an additional viewport to display the document. "ADD_VIEWPORT", + // Rotate the viewport. + "ROTATE_VIEWPORT", + ], module.exports); /** diff --git a/devtools/client/responsive.html/actions/viewports.js b/devtools/client/responsive.html/actions/viewports.js index 673d2524558..0ded91cb232 100644 --- a/devtools/client/responsive.html/actions/viewports.js +++ b/devtools/client/responsive.html/actions/viewports.js @@ -4,7 +4,7 @@ "use strict"; -const { ADD_VIEWPORT } = require("./index"); +const { ADD_VIEWPORT, ROTATE_VIEWPORT } = require("./index"); module.exports = { @@ -17,4 +17,14 @@ module.exports = { }; }, + /** + * Rotate the viewport. + */ + rotateViewport(id) { + return { + type: ROTATE_VIEWPORT, + id, + }; + }, + }; diff --git a/devtools/client/responsive.html/app.js b/devtools/client/responsive.html/app.js index a31398a8d26..c189224a46c 100644 --- a/devtools/client/responsive.html/app.js +++ b/devtools/client/responsive.html/app.js @@ -8,6 +8,7 @@ const { createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react"); const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { rotateViewport } = require("./actions/viewports"); const Types = require("./types"); const Viewports = createFactory(require("./components/viewports")); @@ -22,6 +23,7 @@ let App = createClass({ render() { let { + dispatch, location, viewports, } = this.props; @@ -31,6 +33,7 @@ let App = createClass({ return Viewports({ location, viewports, + onRotateViewport: id => dispatch(rotateViewport(id)), }); }, diff --git a/devtools/client/responsive.html/components/moz.build b/devtools/client/responsive.html/components/moz.build index 1e45f237805..426a572e8d8 100644 --- a/devtools/client/responsive.html/components/moz.build +++ b/devtools/client/responsive.html/components/moz.build @@ -6,6 +6,7 @@ DevToolsModules( 'browser.js', + 'viewport-toolbar.js', 'viewport.js', 'viewports.js', ) diff --git a/devtools/client/responsive.html/components/viewport-toolbar.js b/devtools/client/responsive.html/components/viewport-toolbar.js new file mode 100644 index 00000000000..6e600c0e25c --- /dev/null +++ b/devtools/client/responsive.html/components/viewport-toolbar.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { DOM: dom, createClass, PropTypes } = + require("devtools/client/shared/vendor/react"); + +module.exports = createClass({ + + displayName: "ViewportToolbar", + + propTypes: { + onRotateViewport: PropTypes.func.isRequired, + }, + + render() { + let { + onRotateViewport, + } = this.props; + + return dom.div( + { + className: "viewport-toolbar", + }, + dom.button({ + className: "viewport-rotate-button viewport-toolbar-button", + onClick: onRotateViewport, + }) + ); + }, + +}); diff --git a/devtools/client/responsive.html/components/viewport.js b/devtools/client/responsive.html/components/viewport.js index 7d571d34a50..8ed829a7f09 100644 --- a/devtools/client/responsive.html/components/viewport.js +++ b/devtools/client/responsive.html/components/viewport.js @@ -9,6 +9,7 @@ const { DOM: dom, createClass, createFactory, PropTypes } = const Types = require("../types"); const Browser = createFactory(require("./browser")); +const ViewportToolbar = createFactory(require("./viewport-toolbar")); module.exports = createClass({ @@ -17,22 +18,22 @@ module.exports = createClass({ propTypes: { location: Types.location.isRequired, viewport: PropTypes.shape(Types.viewport).isRequired, + onRotateViewport: PropTypes.func.isRequired, }, render() { let { location, viewport, + onRotateViewport, } = this.props; - // Additional elements will soon appear here around the Browser, like drag - // handles, etc. return dom.div( { className: "viewport" }, - dom.div({ - className: "viewport-header", + ViewportToolbar({ + onRotateViewport, }), Browser({ location, diff --git a/devtools/client/responsive.html/components/viewports.js b/devtools/client/responsive.html/components/viewports.js index 0b2fac51b8f..229b401fc3b 100644 --- a/devtools/client/responsive.html/components/viewports.js +++ b/devtools/client/responsive.html/components/viewports.js @@ -17,23 +17,26 @@ module.exports = createClass({ propTypes: { location: Types.location.isRequired, viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired, + onRotateViewport: PropTypes.func.isRequired, }, render() { let { location, viewports, + onRotateViewport, } = this.props; return dom.div( { id: "viewports", }, - viewports.map((viewport, index) => { + viewports.map(viewport => { return Viewport({ - key: index, + key: viewport.id, location, viewport, + onRotateViewport: () => onRotateViewport(viewport.id), }); }) ); diff --git a/devtools/client/responsive.html/images/moz.build b/devtools/client/responsive.html/images/moz.build new file mode 100644 index 00000000000..c97d4941f60 --- /dev/null +++ b/devtools/client/responsive.html/images/moz.build @@ -0,0 +1,9 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'rotate-viewport.svg', +) diff --git a/devtools/client/responsive.html/images/rotate-viewport.svg b/devtools/client/responsive.html/images/rotate-viewport.svg new file mode 100644 index 00000000000..078f9f4b526 --- /dev/null +++ b/devtools/client/responsive.html/images/rotate-viewport.svg @@ -0,0 +1,6 @@ + + + + diff --git a/devtools/client/responsive.html/index.css b/devtools/client/responsive.html/index.css index 8c5dafe8a40..a7df3e3b6bc 100644 --- a/devtools/client/responsive.html/index.css +++ b/devtools/client/responsive.html/index.css @@ -1,6 +1,18 @@ /* TODO: May break up into component local CSS. Pending future discussions by * React component group on how to best handle CSS. */ +/** + * CSS Variables specific to the responsive design mode + */ + +.theme-light { + --viewport-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26); +} + +.theme-dark { + --viewport-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26); +} + html, body { margin: 0; height: 100%; @@ -42,14 +54,46 @@ body { /* Align all viewports to the top */ vertical-align: top; border: 1px solid var(--theme-splitter-color); - box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26); + box-shadow: var(--viewport-box-shadow); } -.viewport-header { +/** + * Viewport Toolbar + */ + +.viewport-toolbar { background-color: var(--theme-toolbar-background); border-bottom: 1px solid var(--theme-splitter-color); - color: var(--theme-body-color-alt); + color: var(--theme-body-color); + display: flex; + flex-direction: row; + justify-content: flex-end; + height: 18px; +} + +.viewport-toolbar-button { + border: none; + display: block; + margin: 1px 3px; + padding: 0; + width: 16px; height: 16px; + opacity: 0.8; + background-color: var(--theme-body-color); + transition: background 0.25s ease; +} + +.viewport-toolbar-button:hover { + opacity: 1; +} + +.viewport-toolbar-button:active { + background-color: var(--theme-selection-background); + opacity: 1; +} + +.viewport-rotate-button { + mask-image: url("./images/rotate-viewport.svg"); } .browser { diff --git a/devtools/client/responsive.html/moz.build b/devtools/client/responsive.html/moz.build index 324234902c3..4fd8bafb29f 100644 --- a/devtools/client/responsive.html/moz.build +++ b/devtools/client/responsive.html/moz.build @@ -7,6 +7,7 @@ DIRS += [ 'actions', 'components', + 'images', 'reducers', ] diff --git a/devtools/client/responsive.html/reducers/viewports.js b/devtools/client/responsive.html/reducers/viewports.js index f90ae9170fe..177e3861722 100644 --- a/devtools/client/responsive.html/reducers/viewports.js +++ b/devtools/client/responsive.html/reducers/viewports.js @@ -4,10 +4,13 @@ "use strict"; -const { ADD_VIEWPORT } = require("../actions/index"); +const { ADD_VIEWPORT, ROTATE_VIEWPORT } = require("../actions/index"); + +let nextViewportId = 0; const INITIAL_VIEWPORTS = []; const INITIAL_VIEWPORT = { + id: nextViewportId++, width: 320, height: 480, }; @@ -19,7 +22,20 @@ let reducers = { if (viewports.length === 1) { return viewports; } - return [...viewports, INITIAL_VIEWPORT]; + return [...viewports, Object.assign({}, INITIAL_VIEWPORT)]; + }, + + [ROTATE_VIEWPORT](viewports, { id }) { + return viewports.map(viewport => { + if (viewport.id !== id) { + return viewport; + } + + return Object.assign({}, viewport, { + width: viewport.height, + height: viewport.width, + }); + }); }, }; diff --git a/devtools/client/responsive.html/test/unit/test_rotate_viewport.js b/devtools/client/responsive.html/test/unit/test_rotate_viewport.js new file mode 100644 index 00000000000..ad716bad4b9 --- /dev/null +++ b/devtools/client/responsive.html/test/unit/test_rotate_viewport.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test rotating the viewport. + +const { addViewport, rotateViewport } = + require("devtools/client/responsive.html/actions/viewports"); + +add_task(function*() { + let store = Store(); + const { getState, dispatch } = store; + + dispatch(addViewport()); + + let viewport = getState().viewports[0]; + equal(viewport.width, 320, "Default width of 320"); + equal(viewport.height, 480, "Default height of 480"); + + dispatch(rotateViewport(0)); + viewport = getState().viewports[0]; + equal(viewport.width, 480, "Rotated width of 480"); + equal(viewport.height, 320, "Rotated height of 320"); +}); diff --git a/devtools/client/responsive.html/test/unit/xpcshell.ini b/devtools/client/responsive.html/test/unit/xpcshell.ini index dbd638888c3..95d978f5358 100644 --- a/devtools/client/responsive.html/test/unit/xpcshell.ini +++ b/devtools/client/responsive.html/test/unit/xpcshell.ini @@ -6,3 +6,4 @@ firefox-appdir = browser [test_add_viewport.js] [test_change_location.js] +[test_rotate_viewport.js] diff --git a/devtools/client/responsive.html/types.js b/devtools/client/responsive.html/types.js index ce0fbab0cd7..3894628c14a 100644 --- a/devtools/client/responsive.html/types.js +++ b/devtools/client/responsive.html/types.js @@ -14,6 +14,9 @@ const { PropTypes } = require("devtools/client/shared/vendor/react"); */ exports.viewport = { + // The id of the viewport + id: PropTypes.number.isRequired, + // The width of the viewport width: PropTypes.number, diff --git a/devtools/client/webide/modules/simulators.js b/devtools/client/webide/modules/simulators.js index 4ad6a06c2d1..89628f4c716 100644 --- a/devtools/client/webide/modules/simulators.js +++ b/devtools/client/webide/modules/simulators.js @@ -44,13 +44,13 @@ var Simulators = { // If the simulator had a reference to an addon, fix it. if (options.addonID) { - let job = promise.defer(); + let deferred = promise.defer(); AddonManager.getAddonByID(options.addonID, addon => { simulator.addon = addon; delete simulator.options.addonID; - job.resolve(); + deferred.resolve(); }); - jobs.push(job); + jobs.push(deferred.promise); } }); } @@ -232,7 +232,7 @@ var Simulators = { }, emitUpdated() { - this.emit("updated"); + this.emit("updated", { length: this._simulators.length }); this._simulators.sort(LocaleCompare); this._save(); }, diff --git a/devtools/client/webide/test/test_simulators.html b/devtools/client/webide/test/test_simulators.html index fda6e8716d9..235a6009499 100644 --- a/devtools/client/webide/test/test_simulators.html +++ b/devtools/client/webide/test/test_simulators.html @@ -41,6 +41,21 @@ return deferred.promise; } + function waitForUpdate(length) { + info(`Wait for update with length ${length}`); + let deferred = promise.defer(); + let handler = (_, data) => { + if (data.length != length) { + return; + } + info(`Got update with length ${length}`); + Simulators.off("updated", handler); + deferred.resolve(); + }; + Simulators.on("updated", handler); + return deferred.promise; + } + Task.spawn(function* () { let win = yield openWebIDE(false); @@ -83,7 +98,11 @@ sim10.install(); + let updated = waitForUpdate(1); yield addonStatus(sim10, "installed"); + yield updated; + // Wait for next tick to ensure UI elements are updated + yield nextTick(); is(findAll(".runtime-panel-item-simulator").length, 1, "One simulator in runtime panel"); @@ -93,7 +112,11 @@ sim20.install(); + updated = waitForUpdate(2); yield addonStatus(sim20, "installed"); + yield updated; + // Wait for next tick to ensure UI elements are updated + yield nextTick(); is(findAll(".runtime-panel-item-simulator").length, 2, "Two simulators in runtime panel"); @@ -113,6 +136,7 @@ ok(params.args.indexOf("-no-remote") > -1, "Simulator process arguments have --no-remote"); + // Wait for next tick to ensure UI elements are updated yield nextTick(); // Configure the fake 1.0 simulator. @@ -255,6 +279,7 @@ // Configure the fake 2.0 simulator. simulatorList.querySelectorAll(".configure-button")[1].click(); + // Wait for next tick to ensure UI elements are updated yield nextTick(); // Test `name`. @@ -297,11 +322,12 @@ ok(params.args[sid + 1].includes(device.width + "x" + device.height), "Simulator screen resolution looks right"); // Test Simulator Menu. - is(doc.querySelector("#tv_simulator_menu").style.visibility, "hidden", "OpenTVDummyDirectory Button is not hidden\n"); + is(doc.querySelector("#tv_simulator_menu").style.visibility, "hidden", "OpenTVDummyDirectory Button is not hidden"); // Restore default simulator options. doc.querySelector("#reset").click(); + // Wait for next tick to ensure UI elements are updated yield nextTick(); for (let param in defaults.phone) { @@ -314,11 +340,16 @@ sim30tv.install(); + updated = waitForUpdate(3); yield addonStatus(sim30tv, "installed"); + yield updated; + // Wait for next tick to ensure UI elements are updated + yield nextTick(); is(findAll(".runtime-panel-item-simulator").length, 3, "Three simulators in runtime panel"); simulatorList.querySelectorAll(".configure-button")[2].click(); + // Wait for next tick to ensure UI elements are updated yield nextTick(); for (let param in defaults.television) { @@ -333,6 +364,7 @@ Simulators._loadingPromise = null; Simulators._simulators = []; yield Simulators._load(); + // Wait for next tick to ensure UI elements are updated yield nextTick(); is(findAll(".runtime-panel-item-simulator").length, 3, "Three simulators saved and reloaded " + Simulators._simulators.map(s => s.name).join(',')); @@ -354,9 +386,11 @@ // Remove 1.0 simulator. simulatorList.querySelectorAll(".configure-button")[0].click(); + // Wait for next tick to ensure UI elements are updated yield nextTick(); doc.querySelector("#remove").click(); + // Wait for next tick to ensure UI elements are updated yield nextTick(); is(findAll(".runtime-panel-item-simulator").length, 0, "Last simulator was removed"); diff --git a/devtools/server/docs/actor-hierarchy.md b/devtools/server/docs/actor-hierarchy.md index 0356fe27134..28ddd97c4a0 100644 --- a/devtools/server/docs/actor-hierarchy.md +++ b/devtools/server/docs/actor-hierarchy.md @@ -7,22 +7,24 @@ once a parent is removed from the pool, its children are removed as well. The overall hierarchy of actors looks like this: - RootActor: First one, automatically instanciated when we start connecting. - | Mostly meant to instanciate new actors. + RootActor: First one, automatically instantiated when we start connecting. + | Mostly meant to instantiate new actors. | |--> Global-scoped actors: | Actors exposing features related to the main process, - | that are not specific to any document/app/addon. + | that are not specific to any particular context (document, tab, app, + | add-on, or worker). | A good example is the preference actor. | \--> "TabActor" (or alike): - | Actors meant to designate one document, tab, app, addon - | and track its lifetime. + | Actors meant to designate one context (document, tab, app, + | add-on, or worker) and track its lifetime. Generally, there is + | one of these for each thing you can point a toolbox at. | \--> Tab-scoped actors: Actors exposing one particular feature set, this time, - specific to a given document/app/addon. - Like console, inspector actors. + specific to a given context (document, tab, app, add-on, or + worker). Examples include the console and inspector actors. These actors may extend this hierarchy by having their own children, like LongStringActor, WalkerActor, etc. @@ -52,6 +54,15 @@ and returns its `actorID`. That's the main role of RootActor. | Returned by "connect" on RemoteBrowserActor (for tabs) or | "getAppActor" on the Webapps actor (for apps). | + |-- WorkerActor (worker.js) + | Targets a worker (applies to various kinds like web worker, service + | worker, etc.). + | Returned by "listWorkers" request to the root actor to get all workers. + | Returned by "listWorkers" request to a BrowserTabActor to get workers for + | a specific tab. + | Returned by "listWorkers" request to a ChildProcessActor to get workers + | for the chrome of the child process. + | |-- ChromeActor (chrome.js) | Targets all resources in the parent process of firefox | (chrome documents, JSM, JS XPCOM, etc.). @@ -63,23 +74,23 @@ and returns its `actorID`. That's the main role of RootActor. | matching the targeted process. | \-- BrowserAddonActor (addon.js) - Targets the javascript of addons. + Targets the javascript of add-ons. Returned by "listAddons" request. ## "TabActor" Those are the actors exposed by the root actors which are meant to track the -lifetime of a given context: tab, app, process or addon. It also allows -to fetch the tab-scoped actors connected to this context. Actors like console, -inspector, thread (for debugger), styleinspector, etc. Most of them inherit -from TabActor (defined in webbrowser.js) which is document centric. -It automatically tracks the lifetime of the targeted document, but it also -tracks its iframes and allows switching the context to one of its iframes. -For historical reasons, these actors also handle creating the ThreadActor, -used to manage breakpoints in the debugger. All the other tab-scoped actors are -created when we access the TabActor's grip. We return the tab-scoped actors -`actorID` in it. Actors inheriting from TabActor expose `attach`/`detach` -requests, that allows to start/stop the ThreadActor. +lifetime of a given context: tab, app, process, add-on, or worker. It also +allows to fetch the tab-scoped actors connected to this context. Actors like +console, inspector, thread (for debugger), styleinspector, etc. Most of them +inherit from TabActor (defined in webbrowser.js) which is document centric. It +automatically tracks the lifetime of the targeted document, but it also tracks +its iframes and allows switching the context to one of its iframes. For +historical reasons, these actors also handle creating the ThreadActor, used to +manage breakpoints in the debugger. All the other tab-scoped actors are created +when we access the TabActor's grip. We return the tab-scoped actors `actorID` in +it. Actors inheriting from TabActor expose `attach`/`detach` requests, that +allows to start/stop the ThreadActor. The tab-scoped actors expect to find the following properties on the "TabActor": - threadActor: @@ -108,11 +119,11 @@ attributes and events: - chromeEventHandler: The chrome event handler for the current context. Allows to listen to events that can be missing/cancelled on this document itself. -See TabActor documentation for events definition. +See TabActor documentation for events definition. ## Tab-scoped actors -Each of these actors focuses on providing one particular feature set, specific to one context, -that can be a web page, an app, a top level firefox window, a process or an addon resource. - +Each of these actors focuses on providing one particular feature set, specific +to one context, that can be a web page, an app, a top level firefox window, a +process, an add-on, or a worker. diff --git a/devtools/shared/Loader.jsm b/devtools/shared/Loader.jsm index 87d1b7a95a3..536e32325c1 100644 --- a/devtools/shared/Loader.jsm +++ b/devtools/shared/Loader.jsm @@ -254,6 +254,7 @@ this.DevToolsLoader = function DevToolsLoader() { this.lazyImporter = XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils); this.lazyServiceGetter = XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils); this.lazyRequireGetter = this.lazyRequireGetter.bind(this); + this.main = this.main.bind(this); }; DevToolsLoader.prototype = { @@ -390,7 +391,8 @@ DevToolsLoader.prototype = { lazyImporter: this.lazyImporter, lazyServiceGetter: this.lazyServiceGetter, lazyRequireGetter: this.lazyRequireGetter, - id: this.id + id: this.id, + main: this.main }, }; // Lazy define console in order to load Console.jsm only when it is used diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index d6553d5db95..212512b0623 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -936,6 +936,9 @@ pref("layout.accessiblecaret.enabled", true); pref("layout.accessiblecaret.enabled", false); #endif +// Android hides the selection bars at the two ends of the selection highlight. +pref("layout.accessiblecaret.bar.enabled", false); + // Android needs to show the caret when long tapping on an empty content. pref("layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content", true); diff --git a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java index ef0bf7489dd..61acef58dbd 100644 --- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java @@ -557,7 +557,7 @@ public class BrowserApp extends GeckoApp @Override public void onCreate(Bundle savedInstanceState) { - if (!isSupportedSystem()) { + if (!HardwareUtils.isSupportedSystem()) { // This build does not support the Android version of the device; Exit early. super.onCreate(savedInstanceState); return; @@ -1345,7 +1345,7 @@ public class BrowserApp extends GeckoApp @Override public void onDestroy() { - if (!isSupportedSystem()) { + if (!HardwareUtils.isSupportedSystem()) { // This build does not support the Android version of the device; Exit early. super.onDestroy(); return; diff --git a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java index 250304a42ce..f2c7b63a4f1 100644 --- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java @@ -1162,7 +1162,7 @@ public abstract class GeckoApp enableStrictMode(); } - if (!isSupportedSystem()) { + if (!HardwareUtils.isSupportedSystem()) { // This build does not support the Android version of the device: Show an error and finish the app. super.onCreate(savedInstanceState); showSDKVersionError(); @@ -2085,7 +2085,7 @@ public abstract class GeckoApp @Override public void onDestroy() { - if (!isSupportedSystem()) { + if (!HardwareUtils.isSupportedSystem()) { // This build does not support the Android version of the device: // We did not initialize anything, so skip cleaning up. super.onDestroy(); @@ -2193,32 +2193,6 @@ public abstract class GeckoApp } } - protected boolean isSupportedSystem() { - if (Build.VERSION.SDK_INT < Versions.MIN_SDK_VERSION || - Build.VERSION.SDK_INT > Versions.MAX_SDK_VERSION) { - return false; - } - - // See http://developer.android.com/ndk/guides/abis.html - boolean isSystemARM = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("arm"); - boolean isSystemX86 = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("x86"); - - boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("arm"); - boolean isAppX86 = AppConstants.ANDROID_CPU_ARCH.startsWith("x86"); - - // Only reject known incompatible ABIs. Better safe than sorry. - if ((isSystemX86 && isAppARM) || (isSystemARM && isAppX86)) { - return false; - } - - if ((isSystemX86 && isAppX86) || (isSystemARM && isAppARM)) { - return true; - } - - Log.w(LOGTAG, "Unknown app/system ABI combination: " + AppConstants.MOZ_APP_ABI + " / " + Build.CPU_ABI); - return true; - } - public void showSDKVersionError() { final String message = getString(R.string.unsupported_sdk_version, Build.CPU_ABI, Build.VERSION.SDK_INT); Toast.makeText(this, message, Toast.LENGTH_LONG).show(); diff --git a/mobile/android/base/java/org/mozilla/gecko/Restrictions.java b/mobile/android/base/java/org/mozilla/gecko/Restrictions.java index 54079a451bd..30ec53bd9b3 100644 --- a/mobile/android/base/java/org/mozilla/gecko/Restrictions.java +++ b/mobile/android/base/java/org/mozilla/gecko/Restrictions.java @@ -5,21 +5,21 @@ package org.mozilla.gecko; +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.annotation.RobocopTarget; import org.mozilla.gecko.annotation.WrapForJNI; -import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.restrictions.DefaultConfiguration; import org.mozilla.gecko.restrictions.GuestProfileConfiguration; import org.mozilla.gecko.restrictions.Restrictable; import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration; +import org.mozilla.gecko.restrictions.RestrictionCache; import org.mozilla.gecko.restrictions.RestrictionConfiguration; -import android.annotation.TargetApi; -import android.content.Context; -import android.os.Build; -import android.os.UserManager; -import android.util.Log; - @RobocopTarget public class Restrictions { private static final String LOGTAG = "GeckoRestrictedProfiles"; @@ -74,8 +74,7 @@ public class Restrictions { } // The user is on a restricted profile if, and only if, we injected application restrictions during account setup. - final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE); - return !mgr.getApplicationRestrictions(context.getPackageName()).isEmpty(); + return RestrictionCache.hasApplicationRestrictions(context); } public static void update(Context context) { diff --git a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java index 146a2705640..d973a6d3627 100644 --- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java +++ b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java @@ -10,6 +10,7 @@ import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.dlc.catalog.DownloadContent; import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; +import org.mozilla.gecko.util.HardwareUtils; import android.app.IntentService; import android.content.ComponentName; @@ -64,6 +65,12 @@ public class DownloadContentService extends IntentService { return; } + if (!HardwareUtils.isSupportedSystem()) { + // This service is running very early before checks in BrowserApp can prevent us from running. + Log.w(LOGTAG, "System is not supported. Stop."); + return; + } + if (intent == null) { return; } diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java index e47bcf60102..d6c5c1e5caa 100644 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java +++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java @@ -66,9 +66,6 @@ public class RestrictedProfileConfiguration implements RestrictionConfiguration } private Context context; - private Bundle cachedAppRestrictions; - private Bundle cachedUserRestrictions; - private boolean isCacheInvalid = true; public RestrictedProfileConfiguration(Context context) { this.context = context.getApplicationContext(); @@ -76,38 +73,17 @@ public class RestrictedProfileConfiguration implements RestrictionConfiguration @Override public synchronized boolean isAllowed(Restrictable restrictable) { - if (isCacheInvalid || !ThreadUtils.isOnUiThread()) { - readRestrictions(); - isCacheInvalid = false; - } - // Special casing system/user restrictions if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) { - return !cachedUserRestrictions.getBoolean(restrictable.name); + return RestrictionCache.getUserRestriction(context, restrictable.name); } - if (!cachedAppRestrictions.containsKey(restrictable.name) && !configuration.containsKey(restrictable)) { + if (!RestrictionCache.hasApplicationRestriction(context, restrictable.name) && !configuration.containsKey(restrictable)) { // Always allow features that are not in the configuration return true; } - return cachedAppRestrictions.getBoolean(restrictable.name, configuration.get(restrictable)); - } - - private void readRestrictions() { - final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE); - - StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads(); - - try { - Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName()); - migrateRestrictionsIfNeeded(appRestrictions); - - cachedAppRestrictions = appRestrictions; - cachedUserRestrictions = mgr.getUserRestrictions(); - } finally { - StrictMode.setThreadPolicy(policy); - } + return RestrictionCache.getApplicationRestriction(context, restrictable.name, configuration.get(restrictable)); } @Override @@ -135,7 +111,7 @@ public class RestrictedProfileConfiguration implements RestrictionConfiguration @Override public synchronized void update() { - isCacheInvalid = true; + RestrictionCache.invalidate(); } public static List getVisibleRestrictions() { @@ -150,25 +126,4 @@ public class RestrictedProfileConfiguration implements RestrictionConfiguration return visibleList; } - - /** - * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336). - */ - public static void migrateRestrictionsIfNeeded(Bundle bundle) { - if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) { - bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions")); - } - - if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) { - bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing")); - } - - if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) { - bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history")); - } - - if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) { - bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings")); - } - } } diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java new file mode 100644 index 00000000000..523cc113b4c --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java @@ -0,0 +1,99 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.restrictions; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.StrictMode; +import android.os.UserManager; + +import org.mozilla.gecko.util.ThreadUtils; + +/** + * Cache for user and application restrictions. + */ +public class RestrictionCache { + private static Bundle cachedAppRestrictions; + private static Bundle cachedUserRestrictions; + private static boolean isCacheInvalid = true; + + private RestrictionCache() {} + + public static synchronized boolean getUserRestriction(Context context, String restriction) { + updateCacheIfNeeded(context); + return cachedUserRestrictions.getBoolean(restriction); + } + + public static synchronized boolean hasApplicationRestriction(Context context, String restriction) { + updateCacheIfNeeded(context); + return cachedAppRestrictions.containsKey(restriction); + } + + public static synchronized boolean getApplicationRestriction(Context context, String restriction, boolean defaultValue) { + updateCacheIfNeeded(context); + return cachedAppRestrictions.getBoolean(restriction, defaultValue); + } + + public static synchronized boolean hasApplicationRestrictions(Context context) { + updateCacheIfNeeded(context); + return !cachedAppRestrictions.isEmpty(); + } + + public static synchronized void invalidate() { + isCacheInvalid = true; + } + + private static void updateCacheIfNeeded(Context context) { + // If we are not on the UI thread then we can just go ahead and read the values (Bug 1189347). + // Otherwise we read from the cache to avoid blocking the UI thread. If the cache is invalid + // then we hazard the consequences and just do the read. + if (isCacheInvalid || !ThreadUtils.isOnUiThread()) { + readRestrictions(context); + isCacheInvalid = false; + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private static void readRestrictions(Context context) { + final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE); + + // If we do not have anything in the cache yet then this read might happen on the UI thread (Bug 1189347). + final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads(); + + try { + Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName()); + migrateRestrictionsIfNeeded(appRestrictions); + + cachedAppRestrictions = appRestrictions; + cachedUserRestrictions = mgr.getUserRestrictions(); // Always implies disk read + } finally { + StrictMode.setThreadPolicy(policy); + } + } + + /** + * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336). + */ + /* package-private */ static void migrateRestrictionsIfNeeded(Bundle bundle) { + if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) { + bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions")); + } + + if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) { + bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing")); + } + + if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) { + bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history")); + } + + if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) { + bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings")); + } + } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java index 735504e70ed..26b9a446f2f 100644 --- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java +++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java @@ -38,7 +38,7 @@ public class RestrictionProvider extends BroadcastReceiver { @Override public void run() { final Bundle oldRestrictions = intent.getBundleExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE); - RestrictedProfileConfiguration.migrateRestrictionsIfNeeded(oldRestrictions); + RestrictionCache.migrateRestrictionsIfNeeded(oldRestrictions); final Bundle extras = new Bundle(); diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java index c6e32ce3c4f..f52a8560f38 100644 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java +++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java @@ -7,17 +7,18 @@ package org.mozilla.gecko.telemetry; import android.content.Context; import android.os.Build; -import java.io.IOException; -import java.util.Locale; import com.keepsafe.switchboard.SwitchBoard; -import org.json.JSONArray; + import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.Locales; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.telemetry.TelemetryConstants.CorePing; import org.mozilla.gecko.util.StringUtils; +import java.io.IOException; +import java.util.Locale; + /** * A class with static methods to generate the various Java-created Telemetry pings to upload to the telemetry server. */ @@ -87,7 +88,7 @@ public class TelemetryPingGenerator { ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons. ping.put(CorePing.SEQ, seq); if (AppConstants.MOZ_SWITCHBOARD) { - ping.put(CorePing.EXPERIMENTS, getActiveExperiments(context)); + ping.putArray(CorePing.EXPERIMENTS, SwitchBoard.getActiveExperiments(context)); } // TODO (bug 1246816): Remove this "optional" parameter work-around when // GeckoProfile.getAndPersistProfileCreationDateFromFilesystem is implemented. That method returns -1 @@ -97,11 +98,4 @@ public class TelemetryPingGenerator { } return ping; } - - private static JSONArray getActiveExperiments(final Context context) { - if (!AppConstants.MOZ_SWITCHBOARD) { - throw new IllegalStateException("This method should not be called with switchboard disabled"); - } - return new JSONArray(SwitchBoard.getActiveExperiments(context)); - } } diff --git a/mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java b/mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java index d1dafbe856d..def917b1b1b 100644 --- a/mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java +++ b/mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java @@ -5,6 +5,7 @@ package org.mozilla.gecko.util; +import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.SysInfo; import android.content.Context; @@ -97,4 +98,33 @@ public final class HardwareUtils { return memSize < LOW_MEMORY_THRESHOLD_MB; } + + /** + * @return false if the current system is not supported (e.g. APK/system ABI mismatch). + */ + public static boolean isSupportedSystem() { + if (Build.VERSION.SDK_INT < AppConstants.Versions.MIN_SDK_VERSION || + Build.VERSION.SDK_INT > AppConstants.Versions.MAX_SDK_VERSION) { + return false; + } + + // See http://developer.android.com/ndk/guides/abis.html + boolean isSystemARM = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("arm"); + boolean isSystemX86 = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("x86"); + + boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("arm"); + boolean isAppX86 = AppConstants.ANDROID_CPU_ARCH.startsWith("x86"); + + // Only reject known incompatible ABIs. Better safe than sorry. + if ((isSystemX86 && isAppARM) || (isSystemARM && isAppX86)) { + return false; + } + + if ((isSystemX86 && isAppX86) || (isSystemARM && isAppARM)) { + return true; + } + + Log.w(LOGTAG, "Unknown app/system ABI combination: " + AppConstants.MOZ_APP_ABI + " / " + Build.CPU_ABI); + return true; + } } diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 04da2cf0314..f1f93ec4262 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -505,6 +505,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [ 'restrictions/GuestProfileConfiguration.java', 'restrictions/Restrictable.java', 'restrictions/RestrictedProfileConfiguration.java', + 'restrictions/RestrictionCache.java', 'restrictions/RestrictionConfiguration.java', 'restrictions/RestrictionProvider.java', 'ScreenshotObserver.java', diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index b79395b54ac..e739366100e 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -1316,8 +1316,11 @@ var BrowserApp = { let message; let title = closedTabData.entries[closedTabData.index - 1].title; + let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aTab.browser); - if (title) { + if (isPrivate) { + message = Strings.browser.GetStringFromName("privateClosedMessage.message"); + } else if (title) { message = Strings.browser.formatStringFromName("undoCloseToast.message", [title], 1); } else { message = Strings.browser.GetStringFromName("undoCloseToast.messageDefault"); diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties index d70516ee367..f163a28eec2 100644 --- a/mobile/android/locales/en-US/chrome/browser.properties +++ b/mobile/android/locales/en-US/chrome/browser.properties @@ -188,6 +188,11 @@ newtabpopup.switch=SWITCH # when the user closes a tab. %S is the title of the tab that was closed. undoCloseToast.message=Closed %S +# Private Tab closed message +# LOCALIZATION NOTE (privateClosedMessage.message): This message appears +# when the user closes a private tab. +privateClosedMessage.message=Closed Private Browsing + # LOCALIZATION NOTE (undoCloseToast.messageDefault): This message appears in a # toast when the user closes a tab if there is no title to display. undoCloseToast.messageDefault=Closed tab diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java index a236ecdf120..207accc76da 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java @@ -4,21 +4,19 @@ package org.mozilla.gecko.browserid; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.GeneralSecurityException; -import java.util.ArrayList; -import java.util.Map; -import java.util.TreeMap; - import org.json.simple.JSONObject; -import org.json.simple.parser.ParseException; import org.mozilla.apache.commons.codec.binary.Base64; import org.mozilla.apache.commons.codec.binary.StringUtils; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.Utils; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.TreeMap; + /** * Encode and decode JSON Web Tokens. *

@@ -35,14 +33,7 @@ public class JSONWebTokenUtils { public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1"; public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException { - return encode(payload, privateKey, null); - } - - protected static String encode(String payload, SigningPrivateKey privateKey, Map headerFields) throws UnsupportedEncodingException, GeneralSecurityException { - ExtendedJSONObject header = new ExtendedJSONObject(); - if (headerFields != null) { - header.putAll(headerFields); - } + final ExtendedJSONObject header = new ExtendedJSONObject(); header.put("alg", privateKey.getAlgorithm()); String encodedHeader = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8")); String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8")); @@ -78,8 +69,7 @@ public class JSONWebTokenUtils { */ @SuppressWarnings("unchecked") public static String getPayloadString(String payloadString, String audience, String issuer, - Long issuedAt, long expiresAt) throws NonObjectJSONException, - IOException, ParseException { + Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException { ExtendedJSONObject payload; if (payloadString != null) { payload = new ExtendedJSONObject(payloadString); @@ -98,7 +88,7 @@ public class JSONWebTokenUtils { return JSONObject.toJSONString(new TreeMap(payload.object)); } - protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException, ParseException { + protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException { ExtendedJSONObject payload = new ExtendedJSONObject(); ExtendedJSONObject principal = new ExtendedJSONObject(); principal.put("email", email); @@ -108,7 +98,7 @@ public class JSONWebTokenUtils { } public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email, - String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { + String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, GeneralSecurityException { String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email); String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt); return JSONWebTokenUtils.encode(payloadString, privateKey); @@ -135,11 +125,10 @@ public class JSONWebTokenUtils { * @return assertion. * @throws NonObjectJSONException * @throws IOException - * @throws ParseException * @throws GeneralSecurityException */ public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience, - String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { + String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, GeneralSecurityException { String emptyAssertionPayloadString = "{}"; String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt); String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith); diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java index a13a35cc66a..3f2c5620d59 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java @@ -174,7 +174,7 @@ public class AccountPickler { ExtendedJSONObject json = null; try { - json = ExtendedJSONObject.parseJSONObject(jsonString); + json = new ExtendedJSONObject(jsonString); } catch (Exception e) { Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e); return null; diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java index 7d882b5a081..1ec7b4051b5 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.fxa.FxAccountUtils; import org.mozilla.gecko.browserid.BrowserIDKeyPair; import org.mozilla.gecko.browserid.JSONWebTokenUtils; @@ -55,7 +54,7 @@ public class Married extends TokensAndKeysState { delegate.handleTransition(new LogMessage("staying married"), this); } - public String generateAssertion(String audience, String issuer) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException { + public String generateAssertion(String audience, String issuer) throws NonObjectJSONException, IOException, GeneralSecurityException { // We generate assertions with no iat and an exp after 2050 to avoid // invalid-timestamp errors from the token server. final long expiresAt = JSONWebTokenUtils.DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS; diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java index 896649306b0..1fd363bcb00 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java @@ -12,7 +12,6 @@ import java.util.Map.Entry; import java.util.Set; import org.json.simple.JSONArray; -import org.json.simple.parser.ParseException; import org.mozilla.apache.commons.codec.binary.Base64; import org.mozilla.gecko.sync.crypto.CryptoException; import org.mozilla.gecko.sync.crypto.KeyBundle; @@ -108,7 +107,7 @@ public class CollectionKeys { * If non-null, the sync key bundle to decrypt keys with. */ public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle) - throws CryptoException, IOException, ParseException, NonObjectJSONException { + throws CryptoException, IOException, NonObjectJSONException { if (keys == null) { throw new IllegalArgumentException("cannot set key pairs from null record"); } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java index 3adeabd5ec8..65563d3447f 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import org.json.simple.JSONObject; -import org.json.simple.parser.ParseException; import org.mozilla.apache.commons.codec.binary.Base64; import org.mozilla.gecko.sync.crypto.CryptoException; import org.mozilla.gecko.sync.crypto.CryptoInfo; @@ -87,8 +86,9 @@ public class CryptoRecord extends Record { this.payload = payload; } - public CryptoRecord(String jsonString) throws IOException, ParseException, NonObjectJSONException { - this(ExtendedJSONObject.parseJSONObject(jsonString)); + public CryptoRecord(String jsonString) throws IOException, NonObjectJSONException { + + this(new ExtendedJSONObject(jsonString)); } /** @@ -125,11 +125,10 @@ public class CryptoRecord extends Record { * A CryptoRecord that encapsulates the provided record. * * @throws NonObjectJSONException - * @throws ParseException * @throws IOException */ public static CryptoRecord fromJSONRecord(String jsonRecord) - throws ParseException, NonObjectJSONException, IOException, RecordParseException { + throws NonObjectJSONException, IOException, RecordParseException { byte[] bytes = jsonRecord.getBytes("UTF-8"); ExtendedJSONObject object = ExtendedJSONObject.parseUTF8AsJSONObject(bytes); @@ -138,12 +137,12 @@ public class CryptoRecord extends Record { // TODO: defensive programming. public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord) - throws IOException, ParseException, NonObjectJSONException, RecordParseException { + throws IOException, NonObjectJSONException, RecordParseException { String id = (String) jsonRecord.get(KEY_ID); String collection = (String) jsonRecord.get(KEY_COLLECTION); String jsonEncodedPayload = (String) jsonRecord.get(KEY_PAYLOAD); - ExtendedJSONObject payload = ExtendedJSONObject.parseJSONObject(jsonEncodedPayload); + ExtendedJSONObject payload = new ExtendedJSONObject(jsonEncodedPayload); CryptoRecord record = new CryptoRecord(payload); record.guid = id; @@ -181,8 +180,7 @@ public class CryptoRecord extends Record { this.keyBundle = bundle; } - public CryptoRecord decrypt() throws CryptoException, IOException, ParseException, - NonObjectJSONException { + public CryptoRecord decrypt() throws CryptoException, IOException, NonObjectJSONException { if (keyBundle == null) { throw new NoKeyBundleException(); } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java index 04778465c3a..f5fac000999 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java @@ -4,13 +4,6 @@ package org.mozilla.gecko.sync; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; @@ -18,6 +11,14 @@ import org.json.simple.parser.ParseException; import org.mozilla.apache.commons.codec.binary.Base64; import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + /** * Extend JSONObject to do little things, like, y'know, accessing members. * @@ -105,13 +106,17 @@ public class ExtendedJSONObject { * You should prefer the stream interface {@link #parseJSONArray(Reader)}. * * @param jsonString input. - * @throws ParseException * @throws IOException - * @throws NonArrayJSONException if the object is valid JSON, but not an array. + * @throws NonArrayJSONException if the object is invalid JSON or not an array. */ public static JSONArray parseJSONArray(String jsonString) - throws IOException, ParseException, NonArrayJSONException { - Object o = parseRaw(jsonString); + throws IOException, NonArrayJSONException { + Object o = null; + try { + o = parseRaw(jsonString); + } catch (ParseException e) { + throw new NonArrayJSONException(e); + } if (o == null) { return null; @@ -124,45 +129,16 @@ public class ExtendedJSONObject { throw new NonArrayJSONException("value must be a JSON array"); } - /** - * Helper method to get a JSON object from a stream. - * - * @param in input {@link Reader}. - * @throws ParseException - * @throws IOException - * @throws NonArrayJSONException if the object is valid JSON, but not an object. - */ - public static ExtendedJSONObject parseJSONObject(Reader in) - throws IOException, ParseException, NonObjectJSONException { - return new ExtendedJSONObject(in); - } - - /** - * Helper method to get a JSON object from a string. - *

- * You should prefer the stream interface {@link #parseJSONObject(Reader)}. - * - * @param jsonString input. - * @throws ParseException - * @throws IOException - * @throws NonObjectJSONException if the object is valid JSON, but not an object. - */ - public static ExtendedJSONObject parseJSONObject(String jsonString) - throws IOException, ParseException, NonObjectJSONException { - return new ExtendedJSONObject(jsonString); - } - /** * Helper method to get a JSON object from a UTF-8 byte array. * * @param in UTF-8 bytes. - * @throws ParseException - * @throws NonObjectJSONException if the object is valid JSON, but not an object. + * @throws NonObjectJSONException if the object is not valid JSON or not an object. * @throws IOException */ public static ExtendedJSONObject parseUTF8AsJSONObject(byte[] in) - throws ParseException, NonObjectJSONException, IOException { - return parseJSONObject(new String(in, "UTF-8")); + throws NonObjectJSONException, IOException { + return new ExtendedJSONObject(new String(in, "UTF-8")); } public ExtendedJSONObject() { @@ -173,44 +149,19 @@ public class ExtendedJSONObject { this.object = o; } - public ExtendedJSONObject deepCopy() { - final ExtendedJSONObject out = new ExtendedJSONObject(); - @SuppressWarnings("unchecked") - final Set> entries = this.object.entrySet(); - for (Map.Entry entry : entries) { - final String key = entry.getKey(); - final Object value = entry.getValue(); - if (value instanceof JSONArray) { - // Oh god. - try { - out.put(key, new JSONParser().parse(((JSONArray) value).toJSONString())); - } catch (ParseException e) { - // This should never occur, because we're round-tripping. - } - continue; - } - if (value instanceof JSONObject) { - out.put(key, new ExtendedJSONObject((JSONObject) value).deepCopy().object); - continue; - } - if (value instanceof ExtendedJSONObject) { - out.put(key, ((ExtendedJSONObject) value).deepCopy()); - continue; - } - // Oh well. - out.put(key, value); - } - - return out; - } - - public ExtendedJSONObject(Reader in) throws IOException, ParseException, NonObjectJSONException { + public ExtendedJSONObject(Reader in) throws IOException, NonObjectJSONException { if (in == null) { this.object = new JSONObject(); return; } - Object obj = parseRaw(in); + Object obj = null; + try { + obj = parseRaw(in); + } catch (ParseException e) { + throw new NonObjectJSONException(e); + } + if (obj instanceof JSONObject) { this.object = ((JSONObject) obj); } else { @@ -218,7 +169,7 @@ public class ExtendedJSONObject { } } - public ExtendedJSONObject(String jsonString) throws IOException, ParseException, NonObjectJSONException { + public ExtendedJSONObject(String jsonString) throws IOException, NonObjectJSONException { this(jsonString == null ? null : new StringReader(jsonString)); } @@ -319,15 +270,42 @@ public class ExtendedJSONObject { return this.object.toString(); } - public void put(String key, Object value) { + protected void putRaw(String key, Object value) { @SuppressWarnings("unchecked") Map map = this.object; map.put(key, value); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void putAll(Map map) { - this.object.putAll(map); + public void put(String key, String value) { + this.putRaw(key, value); + } + + public void put(String key, boolean value) { + this.putRaw(key, value); + } + + public void put(String key, long value) { + this.putRaw(key, value); + } + + public void put(String key, int value) { + this.putRaw(key, value); + } + + public void put(String key, ExtendedJSONObject value) { + this.putRaw(key, value); + } + + public void put(String key, JSONArray value) { + this.putRaw(key, value); + } + + @SuppressWarnings("unchecked") + public void putArray(String key, List value) { + // Frustratingly inefficient, but there you have it. + final JSONArray jsonArray = new JSONArray(); + jsonArray.addAll(value); + this.putRaw(key, jsonArray); } /** diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java index d13f59560ae..3cadbecca2e 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java @@ -7,13 +7,12 @@ package org.mozilla.gecko.sync; import android.content.Context; import org.json.simple.JSONArray; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.sync.crypto.CryptoException; import org.mozilla.gecko.sync.crypto.KeyBundle; -import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; import org.mozilla.gecko.sync.delegates.FreshStartDelegate; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate; import org.mozilla.gecko.sync.delegates.KeyUploadDelegate; import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate; @@ -102,7 +101,7 @@ public class GlobalSession implements HttpResponseObserver { GlobalSessionCallback callback, Context context, ClientsDataDelegate clientsDelegate) - throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException { + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { if (callback == null) { throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor."); diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java index 62361831a46..a90c0fee82e 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java @@ -13,7 +13,6 @@ import java.util.Map; import java.util.Set; import org.json.simple.JSONArray; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedSyncIDException; import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedVersionException; @@ -56,7 +55,7 @@ public class MetaGlobal implements SyncStorageRequestDelegate { this.isUploading = false; SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.metaURL); r.delegate = this; - r.deferGet(); + r.get(); } catch (URISyntaxException e) { this.callback.handleError(e); } @@ -97,7 +96,7 @@ public class MetaGlobal implements SyncStorageRequestDelegate { return record; } - public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException, NonArrayJSONException { + public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, NonObjectJSONException, NonArrayJSONException { if (record == null) { throw new IllegalArgumentException("Cannot set meta/global from null record"); } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java index 4b4147ffff6..554645b118a 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java @@ -10,4 +10,8 @@ public class NonArrayJSONException extends UnexpectedJSONException { public NonArrayJSONException(String detailMessage) { super(detailMessage); } + + public NonArrayJSONException(Throwable throwable) { + super(throwable); + } } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java index cb3bf7ac0ff..fd50d465e64 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java @@ -10,4 +10,8 @@ public class NonObjectJSONException extends UnexpectedJSONException { public NonObjectJSONException(String detailMessage) { super(detailMessage); } + + public NonObjectJSONException(Throwable throwable) { + super(throwable); + } } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java index e1e6ad76afc..913df1d52d1 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java @@ -171,7 +171,7 @@ public class SyncConfiguration { return null; } try { - final ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json); + final ExtendedJSONObject o = new ExtendedJSONObject(json); return new HashSet(o.keySet()); } catch (Exception e) { return null; @@ -212,7 +212,7 @@ public class SyncConfiguration { return null; } try { - ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json); + ExtendedJSONObject o = new ExtendedJSONObject(json); Map map = new HashMap(); for (Entry e : o.entrySet()) { String key = e.getKey(); diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java index ed46eaa0f5e..2b08be9c4ef 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java @@ -4,14 +4,13 @@ package org.mozilla.gecko.sync; -import java.io.IOException; +import android.content.SharedPreferences.Editor; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.common.PrefsBranch; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; -import android.content.SharedPreferences.Editor; +import java.io.IOException; public class SynchronizerConfiguration { private static final String LOG_TAG = "SynczrConfiguration"; @@ -20,7 +19,7 @@ public class SynchronizerConfiguration { public RepositorySessionBundle remoteBundle; public RepositorySessionBundle localBundle; - public SynchronizerConfiguration(PrefsBranch config) throws NonObjectJSONException, IOException, ParseException { + public SynchronizerConfiguration(PrefsBranch config) throws NonObjectJSONException, IOException { this.load(config); } @@ -31,7 +30,7 @@ public class SynchronizerConfiguration { } // This should get partly shuffled back into SyncConfiguration, I think. - public void load(PrefsBranch config) throws NonObjectJSONException, IOException, ParseException { + public void load(PrefsBranch config) throws NonObjectJSONException, IOException { if (config == null) { throw new IllegalArgumentException("config cannot be null."); } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java index 8c8523eb8c4..e5771452cae 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java @@ -11,6 +11,10 @@ public class UnexpectedJSONException extends Exception { super(detailMessage); } + public UnexpectedJSONException(Throwable throwable) { + super(throwable); + } + public static class BadRequiredFieldJSONException extends UnexpectedJSONException { private static final long serialVersionUID = -9207736984784497612L; diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java index ae7ea459946..37de0f06c7a 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java @@ -34,9 +34,6 @@ import org.mozilla.gecko.sync.setup.Constants; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; -import android.text.Spannable; -import android.text.Spanned; -import android.text.style.ClickableSpan; public class Utils { @@ -425,14 +422,14 @@ public class Utils { ArrayList toSkip = null; if (toSyncString != null) { try { - toSync = new ArrayList(ExtendedJSONObject.parseJSONObject(toSyncString).keySet()); + toSync = new ArrayList(new ExtendedJSONObject(toSyncString).keySet()); } catch (Exception e) { Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e); } } if (toSkipString != null) { try { - toSkip = new ArrayList(ExtendedJSONObject.parseJSONObject(toSkipString).keySet()); + toSkip = new ArrayList(new ExtendedJSONObject(toSkipString).keySet()); } catch (Exception e) { Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e); } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java index 5932fde84d3..bd3cc0af3ca 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java @@ -11,7 +11,6 @@ import java.io.InputStreamReader; import java.io.Reader; import java.util.Scanner; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.NonObjectJSONException; @@ -89,14 +88,12 @@ public class MozResponse { * * @throws IllegalStateException * @throws IOException - * @throws ParseException * @throws NonObjectJSONException */ - public ExtendedJSONObject jsonObjectBody() throws IllegalStateException, IOException, - ParseException, NonObjectJSONException { + public ExtendedJSONObject jsonObjectBody() throws IllegalStateException, IOException, NonObjectJSONException { if (body != null) { // Do it from the cached String. - return ExtendedJSONObject.parseJSONObject(body); + return new ExtendedJSONObject(body); } HttpEntity entity = this.response.getEntity(); @@ -107,7 +104,7 @@ public class MozResponse { InputStream content = entity.getContent(); try { Reader in = new BufferedReader(new InputStreamReader(content, "UTF-8")); - return ExtendedJSONObject.parseJSONObject(in); + return new ExtendedJSONObject(in); } finally { content.close(); } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java index 5e87ce8c8ef..c18c4fe1578 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java @@ -4,14 +4,13 @@ package org.mozilla.gecko.sync.net; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; - import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.mozilla.gecko.sync.CryptoRecord; -import org.mozilla.gecko.sync.ThreadPool; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; /** * Resource class that implements expected headers and processing for Sync. @@ -93,22 +92,4 @@ public class SyncStorageRecordRequest extends SyncStorageRequest { public void put(CryptoRecord record) { this.put(record.toJSONObject()); } - - public void deferGet() { - final SyncStorageRecordRequest self = this; - ThreadPool.run(new Runnable() { - @Override - public void run() { - self.get(); - }}); - } - - public void deferPut(final JSONObject body) { - final SyncStorageRecordRequest self = this; - ThreadPool.run(new Runnable() { - @Override - public void run() { - self.put(body); - }}); - } } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java index 03b63185f42..7908ec7971b 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java @@ -4,13 +4,12 @@ package org.mozilla.gecko.sync.repositories; -import java.io.IOException; - -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.NonObjectJSONException; +import java.io.IOException; + public class RepositorySessionBundle { public static final String LOG_TAG = RepositorySessionBundle.class.getSimpleName(); @@ -18,8 +17,9 @@ public class RepositorySessionBundle { protected final ExtendedJSONObject object; - public RepositorySessionBundle(String jsonString) throws IOException, ParseException, NonObjectJSONException { - object = ExtendedJSONObject.parseJSONObject(jsonString); + public RepositorySessionBundle(String jsonString) throws IOException, NonObjectJSONException { + + object = new ExtendedJSONObject(jsonString); } public RepositorySessionBundle(long lastSyncTimestamp) { diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java index b08a581ad53..9c29953f8da 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java @@ -4,10 +4,14 @@ package org.mozilla.gecko.sync.repositories.android; -import java.io.IOException; +import android.content.ContentProviderClient; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.RemoteException; import org.json.simple.JSONArray; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.sync.ExtendedJSONObject; @@ -16,12 +20,7 @@ import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.domain.ClientRecord; import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; -import android.content.ContentProviderClient; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.RemoteException; +import java.io.IOException; public class RepoUtils { @@ -139,9 +138,6 @@ public class RepoUtils { } catch (IOException e) { Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); return null; - } catch (ParseException e) { - Logger.error(LOG_TAG, "JSON parsing error for " + colId, e); - return null; } } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java index bcb3ca7d063..21ed99d7694 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java @@ -4,12 +4,8 @@ package org.mozilla.gecko.sync.stage; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.Map; -import java.util.concurrent.ExecutorService; +import android.content.Context; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.sync.EngineSettings; import org.mozilla.gecko.sync.GlobalSession; @@ -43,7 +39,10 @@ import org.mozilla.gecko.sync.synchronizer.Synchronizer; import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate; import org.mozilla.gecko.sync.synchronizer.SynchronizerSession; -import android.content.Context; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.concurrent.ExecutorService; /** * Fetch from a server collection into a local repository, encrypting @@ -121,7 +120,7 @@ public abstract class ServerSyncStage extends AbstractSessionManagingSyncStage i } } - protected EngineSettings getEngineSettings() throws NonObjectJSONException, IOException, ParseException { + protected EngineSettings getEngineSettings() throws NonObjectJSONException, IOException { Integer version = getStorageVersion(); if (version == null) { Logger.warn(LOG_TAG, "null storage version for " + this + "; using version 0."); @@ -167,7 +166,7 @@ public abstract class ServerSyncStage extends AbstractSessionManagingSyncStage i return this.getCollection() + "."; } - protected SynchronizerConfiguration getConfig() throws NonObjectJSONException, IOException, ParseException { + protected SynchronizerConfiguration getConfig() throws NonObjectJSONException, IOException { return new SynchronizerConfiguration(session.config.getBranch(bundlePrefix())); } @@ -175,7 +174,7 @@ public abstract class ServerSyncStage extends AbstractSessionManagingSyncStage i synchronizerConfiguration.persist(session.config.getBranch(bundlePrefix())); } - public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException, URISyntaxException, NonObjectJSONException, IOException, ParseException { + public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException, URISyntaxException, NonObjectJSONException, IOException { Repository remote = wrappedServerRepo(); Synchronizer synchronizer = new ServerLocalSynchronizer(); @@ -549,7 +548,7 @@ public abstract class ServerSyncStage extends AbstractSessionManagingSyncStage i } catch (URISyntaxException e) { session.abort(e, "Invalid URI syntax for server repository."); return; - } catch (NonObjectJSONException | ParseException | IOException e) { + } catch (NonObjectJSONException | IOException e) { session.abort(e, "Invalid persisted JSON for config."); return; } diff --git a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java index 93ec0008d55..9ee014dcbcf 100644 --- a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java +++ b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java @@ -171,7 +171,7 @@ public class TokenServerClient { } } } catch (NonArrayJSONException e) { - Logger.warn(LOG_TAG, "Got non-JSON array '" + result.getString(JSON_KEY_ERRORS) + "'.", e); + Logger.warn(LOG_TAG, "Got non-JSON array '" + JSON_KEY_ERRORS + "'.", e); } } diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java index 880ac2718c7..1294e9b7d78 100644 --- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java @@ -3,24 +3,20 @@ package org.mozilla.gecko.background.db; -import java.io.IOException; -import java.util.ArrayList; +import android.database.Cursor; import org.json.simple.JSONArray; import org.json.simple.JSONObject; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; import org.mozilla.gecko.background.sync.helpers.HistoryHelpers; import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonArrayJSONException; -import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataExtender; import org.mozilla.gecko.sync.repositories.android.RepoUtils; import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; -import android.database.Cursor; +import java.util.ArrayList; public class TestAndroidBrowserHistoryDataExtender extends AndroidSyncTestCase { @@ -36,7 +32,7 @@ public class TestAndroidBrowserHistoryDataExtender extends AndroidSyncTestCase { extender.close(); } - public void testStoreFetch() throws NullCursorException, NonObjectJSONException, IOException, ParseException { + public void testStoreFetch() throws Exception { String guid = Utils.generateGuid(); extender.store(Utils.generateGuid(), null); extender.store(guid, null); @@ -55,7 +51,7 @@ public class TestAndroidBrowserHistoryDataExtender extends AndroidSyncTestCase { } } - public void testVisitsForGUID() throws NonArrayJSONException, NonObjectJSONException, IOException, ParseException, NullCursorException { + public void testVisitsForGUID() throws Exception { String guid = Utils.generateGuid(); JSONArray visits = new ExtendedJSONObject("{ \"visits\": [ { \"key\" : \"value\" } ] }").getArray("visits"); diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java index ea157275b4a..52af2ad016e 100644 --- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java @@ -3,9 +3,8 @@ package org.mozilla.gecko.background.sync; -import java.io.IOException; +import android.content.SharedPreferences; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; import org.mozilla.gecko.background.testhelpers.BaseMockServerSyncStage; import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback; @@ -16,11 +15,8 @@ import org.mozilla.gecko.background.testhelpers.WaitHelper; import org.mozilla.gecko.sync.EngineSettings; import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.MetaGlobalException; -import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.SyncConfiguration; -import org.mozilla.gecko.sync.SyncConfigurationException; import org.mozilla.gecko.sync.SynchronizerConfiguration; -import org.mozilla.gecko.sync.crypto.CryptoException; import org.mozilla.gecko.sync.crypto.KeyBundle; import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; import org.mozilla.gecko.sync.net.AuthHeaderProvider; @@ -29,8 +25,6 @@ import org.mozilla.gecko.sync.repositories.domain.Record; import org.mozilla.gecko.sync.stage.NoSuchStageException; import org.mozilla.gecko.sync.synchronizer.Synchronizer; -import android.content.SharedPreferences; - /** * Test the on-device side effects of reset operations on a stage. * @@ -156,8 +150,7 @@ public class TestResetting extends AndroidSyncTestCase { } } - private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException { - + private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws Exception { final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY); final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD); final SharedPreferences prefs = new MockSharedPreferences(); diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java index 1aee113d38a..d2a8b847640 100644 --- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java @@ -3,17 +3,14 @@ package org.mozilla.gecko.background.testhelpers; -import java.io.IOException; -import java.net.URISyntaxException; - -import org.json.simple.parser.ParseException; import org.mozilla.gecko.sync.NoCollectionKeysSetException; -import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.SynchronizerConfiguration; import org.mozilla.gecko.sync.repositories.RecordFactory; import org.mozilla.gecko.sync.repositories.Repository; import org.mozilla.gecko.sync.stage.ServerSyncStage; +import java.net.URISyntaxException; + /** * A stage that joins two Repositories with no wrapping. */ @@ -66,8 +63,7 @@ public abstract class BaseMockServerSyncStage extends ServerSyncStage { return getRemoteRepository(); } - public SynchronizerConfiguration leakConfig() - throws NonObjectJSONException, IOException, ParseException { + public SynchronizerConfiguration leakConfig() throws Exception { return this.getConfig(); } } diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java index 641d4f93115..63afdd1ac05 100644 --- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java @@ -3,10 +3,6 @@ package org.mozilla.gecko.background.testhelpers; -import java.io.IOException; -import java.util.HashMap; - -import org.json.simple.parser.ParseException; import org.mozilla.gecko.sync.EngineSettings; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.SyncConfiguration; @@ -18,15 +14,18 @@ import org.mozilla.gecko.sync.stage.CompletedStage; import org.mozilla.gecko.sync.stage.GlobalSyncStage; import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; +import java.io.IOException; +import java.util.HashMap; + public class MockGlobalSession extends MockPrefsGlobalSession { - public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException { + public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException { this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback); } public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback) - throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException { + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { super(config, callback, null, null); } diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java index c04977a1184..2ff29453f72 100644 --- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java @@ -3,9 +3,9 @@ package org.mozilla.gecko.background.testhelpers; -import java.io.IOException; +import android.content.Context; +import android.content.SharedPreferences; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.SyncConfiguration; @@ -16,8 +16,7 @@ import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; import org.mozilla.gecko.sync.net.AuthHeaderProvider; import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; -import android.content.Context; -import android.content.SharedPreferences; +import java.io.IOException; /** * GlobalSession touches the Android prefs system. Stub that out. @@ -30,7 +29,7 @@ public class MockPrefsGlobalSession extends GlobalSession { SyncConfiguration config, GlobalSessionCallback callback, Context context, ClientsDataDelegate clientsDelegate) throws SyncConfigurationException, IllegalArgumentException, IOException, - ParseException, NonObjectJSONException { + NonObjectJSONException { super(config, callback, context, clientsDelegate); } @@ -39,7 +38,7 @@ public class MockPrefsGlobalSession extends GlobalSession { KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context, ClientsDataDelegate clientsDelegate) throws SyncConfigurationException, IllegalArgumentException, IOException, - ParseException, NonObjectJSONException { + NonObjectJSONException { return getSession(username, new BasicAuthHeaderProvider(username, password), null, syncKeyBundle, callback, context, clientsDelegate); } @@ -49,7 +48,7 @@ public class MockPrefsGlobalSession extends GlobalSession { KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context, ClientsDataDelegate clientsDelegate) throws SyncConfigurationException, IllegalArgumentException, IOException, - ParseException, NonObjectJSONException { + NonObjectJSONException { final SharedPreferences prefs = new MockSharedPreferences(); final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java index a98417afdbc..b45b32b3c0d 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java @@ -6,7 +6,6 @@ package org.mozilla.android.sync.net.test; import ch.boye.httpclientandroidlib.HttpStatus; import org.json.simple.JSONArray; import org.json.simple.JSONObject; -import org.json.simple.parser.ParseException; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -61,14 +60,14 @@ import static org.junit.Assert.fail; public class TestClientsEngineStage extends MockSyncClientsEngineStage { public final static String LOG_TAG = "TestClientsEngSta"; - public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException { + public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, URISyntaxException { super(); session = initializeSession(); } // Static so we can set it during the constructor. This is so evil. private static MockGlobalSessionCallback callback; - private static GlobalSession initializeSession() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException { + private static GlobalSession initializeSession() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, URISyntaxException { callback = new MockGlobalSessionCallback(); SyncConfiguration config = new SyncConfiguration(USERNAME, new BasicAuthHeaderProvider(USERNAME, PASSWORD), new MockSharedPreferences()); config.syncKeyBundle = new KeyBundle(USERNAME, SYNC_KEY); @@ -178,7 +177,6 @@ public class TestClientsEngineStage extends MockSyncClientsEngineStage { throws SyncConfigurationException, IllegalArgumentException, IOException, - ParseException, NonObjectJSONException { super(config, callback); } diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java index bf4759ee512..0f568a81ecd 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java @@ -3,8 +3,6 @@ package org.mozilla.android.sync.net.test; -import ch.boye.httpclientandroidlib.Header; -import org.json.simple.parser.ParseException; import org.junit.Test; import org.junit.runner.RunWith; import org.mozilla.gecko.background.testhelpers.TestRunner; @@ -16,6 +14,8 @@ import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; import java.io.IOException; import java.io.UnsupportedEncodingException; +import ch.boye.httpclientandroidlib.Header; + import static org.junit.Assert.assertEquals; /** @@ -46,7 +46,7 @@ public class TestCredentialsEndToEnd { } @Test - public void testAuthHeaderFromPassword() throws NonObjectJSONException, IOException, ParseException { + public void testAuthHeaderFromPassword() throws NonObjectJSONException, IOException { final ExtendedJSONObject parsed = new ExtendedJSONObject(DESKTOP_PASSWORD_JSON); final String password = parsed.getString("password"); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java index 81313ff9fb7..c00da9b26e5 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java @@ -8,7 +8,6 @@ import ch.boye.httpclientandroidlib.ProtocolVersion; import ch.boye.httpclientandroidlib.message.BasicHttpResponse; import ch.boye.httpclientandroidlib.message.BasicStatusLine; import junit.framework.AssertionFailedError; -import org.json.simple.parser.ParseException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -72,7 +71,7 @@ public class TestGlobalSession { } @Test - public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, NoSuchStageException { + public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, NoSuchStageException { final MockGlobalSessionCallback callback = new MockGlobalSessionCallback(); GlobalSession s = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD, @@ -240,7 +239,7 @@ public class TestGlobalSession { }); } - public MockGlobalSessionCallback doTestSuccess(final boolean stageShouldBackoff, final boolean stageShouldAdvance) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException { + public MockGlobalSessionCallback doTestSuccess(final boolean stageShouldBackoff, final boolean stageShouldAdvance) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException { MockServer server = new MockServer() { @Override public void handle(Request request, Response response) { @@ -295,7 +294,7 @@ public class TestGlobalSession { @Test public void testOnSuccessBackoffAdvanced() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, - ParseException, CryptoException { + CryptoException { MockGlobalSessionCallback callback = doTestSuccess(true, true); assertTrue(callback.calledError); // TODO: this should be calledAborted. @@ -306,7 +305,7 @@ public class TestGlobalSession { @Test public void testOnSuccessBackoffAborted() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, - ParseException, CryptoException { + CryptoException { MockGlobalSessionCallback callback = doTestSuccess(true, false); assertTrue(callback.calledError); // TODO: this should be calledAborted. @@ -317,7 +316,7 @@ public class TestGlobalSession { @Test public void testOnSuccessNoBackoffAdvanced() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, - ParseException, CryptoException { + CryptoException { MockGlobalSessionCallback callback = doTestSuccess(false, true); assertTrue(callback.calledSuccess); @@ -327,7 +326,7 @@ public class TestGlobalSession { @Test public void testOnSuccessNoBackoffAborted() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, - ParseException, CryptoException { + CryptoException { MockGlobalSessionCallback callback = doTestSuccess(false, false); assertTrue(callback.calledError); // TODO: this should be calledAborted. @@ -396,7 +395,7 @@ public class TestGlobalSession { ExtendedJSONObject origEnginesJSONObject = new ExtendedJSONObject(); for (String engineName : origEngines) { EngineSettings mockEngineSettings = new EngineSettings(Utils.generateGuid(), Integer.valueOf(0)); - origEnginesJSONObject.put(engineName, mockEngineSettings); + origEnginesJSONObject.put(engineName, mockEngineSettings.toJSONObject()); } session.config.metaGlobal.setEngines(origEnginesJSONObject); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java index 7c6602eeeaf..ec4c038593b 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java @@ -3,7 +3,6 @@ package org.mozilla.android.sync.net.test; -import org.json.simple.parser.ParseException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -14,6 +13,7 @@ import org.mozilla.gecko.background.testhelpers.WaitHelper; import org.mozilla.gecko.sync.CryptoRecord; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.MetaGlobal; +import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate; import org.mozilla.gecko.sync.net.BaseResource; import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; @@ -33,8 +33,6 @@ import static org.junit.Assert.assertTrue; @RunWith(TestRunner.class) public class TestMetaGlobal { - public static Object monitor = new Object(); - private static final int TEST_PORT = HTTPServerTestHelper.getTestPort(); private static final String TEST_SERVER = "http://localhost:" + TEST_PORT; private static final String TEST_SYNC_ID = "foobar"; @@ -223,7 +221,7 @@ public class TestMetaGlobal { assertTrue(delegate.errorCalled); assertNotNull(delegate.errorException); - assertEquals(ParseException.class, delegate.errorException.getClass()); + assertEquals(NonObjectJSONException.class, delegate.errorException.getClass()); } @SuppressWarnings("static-method") @@ -319,7 +317,7 @@ public class TestMetaGlobal { public void handle(Request request, Response response) { if (request.getMethod().equals("PUT")) { try { - ExtendedJSONObject body = ExtendedJSONObject.parseJSONObject(request.getContent()); + ExtendedJSONObject body = new ExtendedJSONObject(request.getContent()); System.out.println(body.toJSONString()); assertTrue(body.containsKey("payload")); assertFalse(body.containsKey("default")); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java index 1e05f9bcf93..76791a6edcf 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java @@ -4,7 +4,6 @@ package org.mozilla.android.sync.test; import org.json.simple.JSONArray; -import org.json.simple.parser.ParseException; import org.junit.Test; import org.junit.runner.RunWith; import org.mozilla.apache.commons.codec.binary.Base64; @@ -71,7 +70,7 @@ public class TestCollectionKeys { @Test - public void testSetKeysFromWBO() throws IOException, ParseException, NonObjectJSONException, CryptoException, NoCollectionKeysSetException { + public void testSetKeysFromWBO() throws IOException, NonObjectJSONException, CryptoException, NoCollectionKeysSetException { String json = "{\"default\":[\"3fI6k1exImMgAKjilmMaAWxGqEIzFX/9K5EjEgH99vc=\",\"/AMaoCX4hzic28WY94XtokNi7N4T0nv+moS1y5wlbug=\"],\"collections\":{},\"collection\":\"crypto\",\"id\":\"keys\"}"; CryptoRecord rec = new CryptoRecord(json); @@ -87,7 +86,7 @@ public class TestCollectionKeys { } @Test - public void testCryptoRecordFromCollectionKeys() throws CryptoException, NoCollectionKeysSetException, IOException, ParseException, NonObjectJSONException { + public void testCryptoRecordFromCollectionKeys() throws CryptoException, NoCollectionKeysSetException, IOException, NonObjectJSONException { CollectionKeys ck1 = CollectionKeys.generateCollectionKeys(); assertNotNull(ck1.defaultKeyBundle()); assertEquals(ck1.keyBundleForCollection("foobar"), ck1.defaultKeyBundle()); @@ -103,7 +102,7 @@ public class TestCollectionKeys { } @Test - public void testCreateKeysBundle() throws CryptoException, NonObjectJSONException, IOException, ParseException, NoCollectionKeysSetException { + public void testCreateKeysBundle() throws CryptoException, NonObjectJSONException, IOException, NoCollectionKeysSetException { String username = "b6evr62dptbxz7fvebek7btljyu322wp"; String friendlyBase32SyncKey = "basuxv2426eqj7frhvpcwkavdi"; diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java index 02d2eadd2b8..adab2d7380b 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java @@ -3,7 +3,6 @@ package org.mozilla.android.sync.test; -import org.json.simple.parser.ParseException; import org.junit.Test; import org.junit.runner.RunWith; import org.mozilla.gecko.background.testhelpers.TestRunner; @@ -49,14 +48,14 @@ public class TestCommandProcessor extends CommandProcessor { } @Test - public void testRegisterCommand() throws NonObjectJSONException, IOException, ParseException { + public void testRegisterCommand() throws NonObjectJSONException, IOException { assertNull(commands.get(commandType)); this.registerCommand(commandType, new MockCommandRunner(1)); assertNotNull(commands.get(commandType)); } @Test - public void testProcessRegisteredCommand() throws NonObjectJSONException, IOException, ParseException { + public void testProcessRegisteredCommand() throws NonObjectJSONException, IOException { commandExecuted = false; ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand); this.registerCommand(commandType, new MockCommandRunner(1)); @@ -65,7 +64,7 @@ public class TestCommandProcessor extends CommandProcessor { } @Test - public void testProcessUnregisteredCommand() throws NonObjectJSONException, IOException, ParseException { + public void testProcessUnregisteredCommand() throws NonObjectJSONException, IOException { commandExecuted = false; ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand); this.processCommand(session, unparsedCommand); @@ -73,7 +72,7 @@ public class TestCommandProcessor extends CommandProcessor { } @Test - public void testProcessInvalidCommand() throws NonObjectJSONException, IOException, ParseException { + public void testProcessInvalidCommand() throws NonObjectJSONException, IOException { ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType); this.registerCommand(commandType, new MockCommandRunner(1)); this.processCommand(session, unparsedCommand); @@ -81,19 +80,19 @@ public class TestCommandProcessor extends CommandProcessor { } @Test - public void testParseCommandNoType() throws NonObjectJSONException, IOException, ParseException { + public void testParseCommandNoType() throws NonObjectJSONException, IOException { ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType); assertNull(CommandProcessor.parseCommand(unparsedCommand)); } @Test - public void testParseCommandNoArgs() throws NonObjectJSONException, IOException, ParseException { + public void testParseCommandNoArgs() throws NonObjectJSONException, IOException { ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoArgs); assertNull(CommandProcessor.parseCommand(unparsedCommand)); } @Test - public void testParseWellFormedCommand() throws NonObjectJSONException, IOException, ParseException { + public void testParseWellFormedCommand() throws NonObjectJSONException, IOException { ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand); Command parsedCommand = CommandProcessor.parseCommand(unparsedCommand); assertNotNull(parsedCommand); @@ -102,7 +101,7 @@ public class TestCommandProcessor extends CommandProcessor { } @Test - public void testParseCommandNullArg() throws NonObjectJSONException, IOException, ParseException { + public void testParseCommandNullArg() throws NonObjectJSONException, IOException { ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommandWithNullArgs); Command parsedCommand = CommandProcessor.parseCommand(unparsedCommand); assertNotNull(parsedCommand); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java index 125d82c6757..a6b91eaf8a0 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java @@ -5,7 +5,6 @@ package org.mozilla.android.sync.test; import org.json.simple.JSONArray; import org.json.simple.JSONObject; -import org.json.simple.parser.ParseException; import org.junit.Test; import org.junit.runner.RunWith; import org.mozilla.apache.commons.codec.binary.Base64; @@ -35,8 +34,14 @@ public class TestCryptoRecord { String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqPlq/QQXEjx70="; @Test - public void testBaseCryptoRecordEncrypt() throws IOException, ParseException, NonObjectJSONException, CryptoException { - ExtendedJSONObject clearPayload = ExtendedJSONObject.parseJSONObject("{\"id\":\"5qRsgXWRJZXr\",\"title\":\"Index of file:///Users/jason/Library/Application Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"histUri\":\"file:///Users/jason/Library/Application%20Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"visits\":[{\"type\":1,\"date\":1319149012372425}]}"); + public void testBaseCryptoRecordEncrypt() throws IOException, NonObjectJSONException, CryptoException { + + ExtendedJSONObject clearPayload = new ExtendedJSONObject("{\"id\":\"5qRsgXWRJZXr\"," + + "\"title\":\"Index of file:///Users/jason/Library/Application " + + "Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\"," + + "\"histUri\":\"file:///Users/jason/Library/Application%20Support/Firefox/Profiles" + + "/ksgd7wpk.LocalSyncServer/weave/logs/\",\"visits\":[{\"type\":1," + + "\"date\":1319149012372425}]}"); CryptoRecord record = new CryptoRecord(); record.payload = clearPayload; @@ -256,7 +261,7 @@ public class TestCryptoRecord { assertEquals(expectedJson.get("collections"), decrypted.payload.get("collections")); // Check that the extracted keys were as expected. - JSONArray keys = ExtendedJSONObject.parseJSONObject(decrypted.payload.toJSONString()).getArray("default"); + JSONArray keys = new ExtendedJSONObject(decrypted.payload.toJSONString()).getArray("default"); KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys((String)keys.get(0), (String)keys.get(1)); assertArrayEquals(Base64.decodeBase64(expectedBase64EncryptionKey.getBytes("UTF-8")), keyBundle.getEncryptionKey()); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java index 82b183b75fc..473534aac50 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java @@ -5,7 +5,6 @@ package org.mozilla.android.sync.test; import org.json.simple.JSONArray; import org.json.simple.JSONObject; -import org.json.simple.parser.ParseException; import org.junit.Test; import org.junit.runner.RunWith; import org.mozilla.gecko.background.db.Tab; @@ -33,7 +32,7 @@ public class TestRecord { @SuppressWarnings("static-method") @Test - public void testQueryRecord() throws NonObjectJSONException, IOException, ParseException { + public void testQueryRecord() throws NonObjectJSONException, IOException { final String expectedGUID = "Bl3n3gpKag3s"; final String testRecord = "{\"id\":\"" + expectedGUID + "\"," + @@ -193,7 +192,7 @@ public class TestRecord { " \"urlHistory\":[\"http://mxr.mozilla.org/mozilla-central/source/browser/base/content/syncSetup.js#72\"]," + " \"icon\":\"http://mxr.mozilla.org/mxr.png\"," + " \"lastUsed\":\"1306374531\"}"; - Tab tab = TabsRecord.tabFromJSONObject(ExtendedJSONObject.parseJSONObject(json).object); + Tab tab = TabsRecord.tabFromJSONObject(new ExtendedJSONObject(json).object); assertEquals("mozilla-central mozilla/browser/base/content/syncSetup.js", tab.title); assertEquals("http://mxr.mozilla.org/mxr.png", tab.icon); @@ -204,7 +203,7 @@ public class TestRecord { " \"urlHistory\":[\"http://example.com\"]," + " \"icon\":\"\"," + " \"lastUsed\":0}"; - Tab zero = TabsRecord.tabFromJSONObject(ExtendedJSONObject.parseJSONObject(zeroJSON).object); + Tab zero = TabsRecord.tabFromJSONObject(new ExtendedJSONObject(zeroJSON).object); assertEquals("a", zero.title); assertEquals("", zero.icon); @@ -280,7 +279,7 @@ public class TestRecord { super("abcdefghijkl", "bookmarks", 1234, false); } - public void doTest() throws NonObjectJSONException, IOException, ParseException { + public void doTest() throws NonObjectJSONException, IOException { this.initFromPayload(new ExtendedJSONObject(payload)); assertEquals("abcdefghijkl", this.guid); // Ignores payload. assertEquals("livemark", this.type); @@ -297,7 +296,7 @@ public class TestRecord { } @Test - public void testUnusualBookmarkRecords() throws NonObjectJSONException, IOException, ParseException { + public void testUnusualBookmarkRecords() throws NonObjectJSONException, IOException { PayloadBookmarkRecord record = new PayloadBookmarkRecord(); record.doTest(); } diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java index f873b7a5b0a..22bcc509364 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java @@ -4,7 +4,6 @@ package org.mozilla.android.sync.test; import android.content.SharedPreferences; -import org.json.simple.parser.ParseException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,7 +61,7 @@ public class TestResetCommands { } @Test - public void testHandleResetCommand() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException { + public void testHandleResetCommand() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException { // Create a global session. // Set up stage mappings for a real stage name (because they're looked up by name // in an enumeration) pointing to our fake stage. diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java index 042a9fab3f6..d9aa936f04f 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java @@ -3,7 +3,6 @@ package org.mozilla.gecko.background.testhelpers; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.sync.NoCollectionKeysSetException; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.SynchronizerConfiguration; @@ -67,7 +66,7 @@ public abstract class BaseMockServerSyncStage extends ServerSyncStage { } public SynchronizerConfiguration leakConfig() - throws NonObjectJSONException, IOException, ParseException { + throws NonObjectJSONException, IOException { return this.getConfig(); } } diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java index 8f6ed1530a9..63afdd1ac05 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java @@ -3,7 +3,6 @@ package org.mozilla.gecko.background.testhelpers; -import org.json.simple.parser.ParseException; import org.mozilla.gecko.sync.EngineSettings; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.SyncConfiguration; @@ -21,12 +20,12 @@ import java.util.HashMap; public class MockGlobalSession extends MockPrefsGlobalSession { - public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException { + public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException { this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback); } public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback) - throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException { + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { super(config, callback, null, null); } diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java index 2346e897490..c864cdf80c2 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java @@ -5,7 +5,7 @@ package org.mozilla.gecko.background.testhelpers; import android.content.Context; import android.content.SharedPreferences; -import org.json.simple.parser.ParseException; + import org.mozilla.gecko.sync.GlobalSession; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.SyncConfiguration; @@ -28,8 +28,7 @@ public class MockPrefsGlobalSession extends GlobalSession { public MockPrefsGlobalSession( SyncConfiguration config, GlobalSessionCallback callback, Context context, ClientsDataDelegate clientsDelegate) - throws SyncConfigurationException, IllegalArgumentException, IOException, - ParseException, NonObjectJSONException { + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { super(config, callback, context, clientsDelegate); } @@ -37,8 +36,7 @@ public class MockPrefsGlobalSession extends GlobalSession { String username, String password, KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context, ClientsDataDelegate clientsDelegate) - throws SyncConfigurationException, IllegalArgumentException, IOException, - ParseException, NonObjectJSONException { + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { return getSession(username, new BasicAuthHeaderProvider(username, password), null, syncKeyBundle, callback, context, clientsDelegate); } @@ -47,8 +45,7 @@ public class MockPrefsGlobalSession extends GlobalSession { String username, AuthHeaderProvider authHeaderProvider, String prefsPath, KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context, ClientsDataDelegate clientsDelegate) - throws SyncConfigurationException, IllegalArgumentException, IOException, - ParseException, NonObjectJSONException { + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { final SharedPreferences prefs = new MockSharedPreferences(); final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java index 306178fe83f..d38a4caf2f1 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java @@ -4,7 +4,6 @@ package org.mozilla.gecko.sync.middleware.test; import junit.framework.AssertionFailedError; -import org.json.simple.parser.ParseException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -161,7 +160,7 @@ public class TestCrypto5MiddlewareRepositorySession { /** * Verify that store is actually writing encrypted data to the underlying repository. */ - public void testStoreEncrypts() throws NonObjectJSONException, CryptoException, IOException, ParseException { + public void testStoreEncrypts() throws NonObjectJSONException, CryptoException, IOException { final BookmarkRecord record = new BookmarkRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false); record.title = "unencrypted title"; diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java index a50db270622..cb74b427ba0 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java @@ -87,7 +87,7 @@ public class TestEnsureCrypto5KeysStage { session.config.setClusterURL(new URI(TEST_CLUSTER_URL)); // Set info collections to not have crypto. - final ExtendedJSONObject noCrypto = ExtendedJSONObject.parseJSONObject(TEST_JSON_NO_CRYPTO); + final ExtendedJSONObject noCrypto = new ExtendedJSONObject(TEST_JSON_NO_CRYPTO); session.config.infoCollections = new InfoCollections(noCrypto); calledResetStages = false; stagesReset = null; @@ -113,7 +113,8 @@ public class TestEnsureCrypto5KeysStage { @Test public void testDownloadUsesPersisted() throws Exception { - session.config.infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_JSON_OLD_CRYPTO)); + session.config.infoCollections = new InfoCollections(new ExtendedJSONObject + (TEST_JSON_OLD_CRYPTO)); session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis()); assertNull(session.config.collectionKeys); @@ -136,7 +137,7 @@ public class TestEnsureCrypto5KeysStage { @Test public void testDownloadFetchesNew() throws Exception { - session.config.infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_JSON_NEW_CRYPTO)); + session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO)); session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis()); assertNull(session.config.collectionKeys); @@ -172,7 +173,7 @@ public class TestEnsureCrypto5KeysStage { public void testDownloadResetsOnDifferentDefaultKey() throws Exception { String TEST_COLLECTION = "bookmarks"; - session.config.infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_JSON_NEW_CRYPTO)); + session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO)); session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis()); KeyBundle keyBundle = KeyBundle.withRandomKeys(); @@ -212,7 +213,7 @@ public class TestEnsureCrypto5KeysStage { public void testDownloadResetsEngineOnDifferentKey() throws Exception { final String TEST_COLLECTION = "history"; - session.config.infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_JSON_NEW_CRYPTO)); + session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO)); session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis()); assertNull(session.config.collectionKeys); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java index de35701b6b7..f7ed7a559b7 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java @@ -4,7 +4,6 @@ package org.mozilla.gecko.sync.stage.test; import org.json.simple.JSONArray; -import org.json.simple.parser.ParseException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -93,7 +92,7 @@ public class TestFetchMetaGlobalStage { calledResetAllStages = false; // Set info collections to not have crypto. - infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_INFO_COLLECTIONS_JSON)); + infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_INFO_COLLECTIONS_JSON)); syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY); callback = new MockGlobalSessionCallback(); @@ -335,7 +334,7 @@ public class TestFetchMetaGlobalStage { doSession(server); assertEquals(true, callback.calledError); - assertEquals(ParseException.class, callback.calledErrorException.getClass()); + assertEquals(NonObjectJSONException.class, callback.calledErrorException.getClass()); } protected void doFreshStart(MockServer server) { @@ -350,7 +349,7 @@ public class TestFetchMetaGlobalStage { } @Test - public void testFreshStart() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException { + public void testFreshStart() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException { final AtomicBoolean mgUploaded = new AtomicBoolean(false); final AtomicBoolean mgDownloaded = new AtomicBoolean(false); final MetaGlobal uploadedMg = new MetaGlobal(null, null); @@ -360,7 +359,7 @@ public class TestFetchMetaGlobalStage { public void handle(Request request, Response response) { if (request.getMethod().equals("PUT")) { try { - ExtendedJSONObject body = ExtendedJSONObject.parseJSONObject(request.getContent()); + ExtendedJSONObject body = new ExtendedJSONObject(request.getContent()); assertTrue(body.containsKey("payload")); assertFalse(body.containsKey("default")); diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java index 4b6a204f534..cff9287df5d 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java @@ -5,7 +5,6 @@ package org.mozilla.gecko.sync.test; import org.json.simple.JSONArray; import org.json.simple.JSONObject; -import org.json.simple.parser.ParseException; import org.junit.Test; import org.junit.runner.RunWith; import org.mozilla.gecko.background.testhelpers.TestRunner; @@ -20,6 +19,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; @@ -33,18 +33,7 @@ public class TestExtendedJSONObject { public static String exampleIntegral = "{\"modified\":1233702554,}"; @Test - public void testDeepCopy() throws NonObjectJSONException, IOException, ParseException, NonArrayJSONException { - ExtendedJSONObject a = new ExtendedJSONObject(exampleJSON); - ExtendedJSONObject c = a.deepCopy(); - assertTrue(a != c); - assertTrue(a.equals(c)); - assertTrue(a.get("modified") == c.get("modified")); - assertTrue(a.getArray("success") != c.getArray("success")); - assertTrue(a.getArray("success").equals(c.getArray("success"))); - } - - @Test - public void testFractional() throws IOException, ParseException, NonObjectJSONException { + public void testFractional() throws IOException, NonObjectJSONException { ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON); assertTrue(o.containsKey("modified")); assertTrue(o.containsKey("success")); @@ -59,7 +48,7 @@ public class TestExtendedJSONObject { } @Test - public void testIntegral() throws IOException, ParseException, NonObjectJSONException { + public void testIntegral() throws IOException, NonObjectJSONException { ExtendedJSONObject o = new ExtendedJSONObject(exampleIntegral); assertTrue(o.containsKey("modified")); assertFalse(o.containsKey("success")); @@ -73,10 +62,9 @@ public class TestExtendedJSONObject { public void testSafeInteger() { ExtendedJSONObject o = new ExtendedJSONObject(); o.put("integer", Integer.valueOf(5)); - o.put("double", Double.valueOf(1.2)); o.put("string", "66"); o.put("object", new ExtendedJSONObject()); - o.put("null", null); + o.put("null", (JSONArray) null); assertEquals(Integer.valueOf(5), o.getIntegerSafely("integer")); assertEquals(Integer.valueOf(66), o.getIntegerSafely("string")); @@ -98,7 +86,7 @@ public class TestExtendedJSONObject { try { ExtendedJSONObject.parseJSONArray("[0, "); fail(); - } catch (ParseException e) { + } catch (NonArrayJSONException e) { // Do nothing. } @@ -124,14 +112,14 @@ public class TestExtendedJSONObject { try { ExtendedJSONObject.parseUTF8AsJSONObject("{}".getBytes("UTF-16")); fail(); - } catch (ParseException e) { + } catch (NonObjectJSONException e) { // Do nothing. } try { ExtendedJSONObject.parseUTF8AsJSONObject("{".getBytes("UTF-8")); fail(); - } catch (ParseException e) { + } catch (NonObjectJSONException e) { // Do nothing. } } @@ -157,8 +145,7 @@ public class TestExtendedJSONObject { ExtendedJSONObject q = new ExtendedJSONObject(exampleJSON); q.put("modified", 0); assertNotSame(o, q); - q.put("modified", o.get("modified")); - assertEquals(o, q); + assertNotEquals(o, q); } @Test diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java index 010db749344..d850ccc5688 100644 --- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java @@ -49,7 +49,7 @@ public class TestInfoCollections { InfoCounts infoCountsEmpty = new InfoCounts(new ExtendedJSONObject("{}")); assertEquals(null, infoCountsEmpty.getCount("bookmarks")); - ExtendedJSONObject record = ExtendedJSONObject.parseJSONObject(TEST_COUNTS_JSON); + ExtendedJSONObject record = new ExtendedJSONObject(TEST_COUNTS_JSON); InfoCounts infoCountsFull = new InfoCounts(record); assertEquals(Integer.valueOf(766), infoCountsFull.getCount("bookmarks")); assertEquals(null, infoCountsFull.getCount("notpresent")); @@ -59,7 +59,7 @@ public class TestInfoCollections { @SuppressWarnings("static-method") @Test public void testSetCollectionsFromRecord() throws Exception { - ExtendedJSONObject record = ExtendedJSONObject.parseJSONObject(TEST_COLLECTIONS_JSON); + ExtendedJSONObject record = new ExtendedJSONObject(TEST_COLLECTIONS_JSON); InfoCollections infoCollections = new InfoCollections(record); assertEquals(Utils.decimalSecondsToMilliseconds(1.3319567131E9), infoCollections.getTimestamp("history").longValue()); @@ -71,7 +71,7 @@ public class TestInfoCollections { @SuppressWarnings("static-method") @Test public void testUpdateNeeded() throws Exception { - ExtendedJSONObject record = ExtendedJSONObject.parseJSONObject(TEST_COLLECTIONS_JSON); + ExtendedJSONObject record = new ExtendedJSONObject(TEST_COLLECTIONS_JSON); InfoCollections infoCollections = new InfoCollections(record); long none = -1; diff --git a/services/common/hawkclient.js b/services/common/hawkclient.js index 7fad7fb99bb..42ed5ff0c70 100644 --- a/services/common/hawkclient.js +++ b/services/common/hawkclient.js @@ -190,13 +190,16 @@ this.HawkClient.prototype = { * @param payloadObj * An object that can be encodable as JSON as the payload of the * request + * @param extraHeaders + * An object with header/value pairs to send with the request. * @return Promise * Returns a promise that resolves to the response of the API call, * or is rejected with an error. If the server response can be parsed * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - request: function(path, method, credentials=null, payloadObj={}, retryOK=true) { + request: function(path, method, credentials=null, payloadObj={}, extraHeaders = {}, + retryOK=true) { method = method.toLowerCase(); let deferred = Promise.defer(); @@ -237,7 +240,7 @@ this.HawkClient.prototype = { // Clock offset is adjusted already in the top of this function. log.debug("Received 401 for " + path + ": retrying"); return deferred.resolve( - self.request(path, method, credentials, payloadObj, false)); + self.request(path, method, credentials, payloadObj, extraHeaders, false)); } // If the server returned a json error message, use it in the rejection @@ -278,6 +281,7 @@ this.HawkClient.prototype = { let extra = { now: this.now(), localtimeOffsetMsec: this.localtimeOffsetMsec, + headers: extraHeaders }; let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra); diff --git a/services/common/hawkrequest.js b/services/common/hawkrequest.js index 7cd31a30661..454960b7b95 100644 --- a/services/common/hawkrequest.js +++ b/services/common/hawkrequest.js @@ -42,7 +42,9 @@ const Prefs = new Preferences("services.common.rest."); * Valid properties are: * * now: , - * localtimeOffsetMsec: + * localtimeOffsetMsec: , + * headers: * * extra.localtimeOffsetMsec is the value in milliseconds that must be added to * the local clock to make it agree with the server's clock. For instance, if @@ -58,6 +60,7 @@ this.HAWKAuthenticatedRESTRequest = this.now = extra.now || Date.now(); this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0; this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec)); + this.extraHeaders = extra.headers || {}; // Expose for testing this._intl = getIntl(); @@ -83,6 +86,10 @@ HAWKAuthenticatedRESTRequest.prototype = { this._log.trace("hawk auth header: " + header.field); } + for (let header in this.extraHeaders) { + this.setHeader(header, this.extraHeaders[header]); + } + this.setHeader("Content-Type", contentType); this.setHeader("Accept-Language", this._intl.accept_languages); diff --git a/services/common/tests/unit/test_hawkclient.js b/services/common/tests/unit/test_hawkclient.js index 95375c7affb..0896cf00c84 100644 --- a/services/common/tests/unit/test_hawkclient.js +++ b/services/common/tests/unit/test_hawkclient.js @@ -98,6 +98,29 @@ add_task(function test_authenticated_patch_request() { check_authenticated_request("PATCH"); }); +add_task(function* test_extra_headers() { + let server = httpd_setup({"/foo": (request, response) => { + do_check_true(request.hasHeader("Authorization")); + do_check_true(request.hasHeader("myHeader")); + do_check_eq(request.getHeader("myHeader"), "fake"); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json"); + response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available()); + } + }); + + let client = new HawkClient(server.baseURI); + + let response = yield client.request("/foo", "POST", TEST_CREDS, {foo: "bar"}, + {"myHeader": "fake"}); + let result = JSON.parse(response.body); + + do_check_eq("bar", result.foo); + + yield deferredStop(server); +}); + add_task(function* test_credentials_optional() { let method = "GET"; let server = httpd_setup({ diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js index d12ecc9f6bf..da03bbe531a 100644 --- a/storage/test/unit/test_storage_connection.js +++ b/storage/test/unit/test_storage_connection.js @@ -68,12 +68,8 @@ add_task(function* test_indexExists_created() { add_task(function* test_createTable_already_created() { var msc = getOpenedDatabase(); do_check_true(msc.tableExists("test")); - try { - msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); - do_throw("We shouldn't get here!"); - } catch (e) { - do_check_eq(Cr.NS_ERROR_FAILURE, e.result); - } + Assert.throws(() => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/); }); add_task(function* test_attach_createTable_tableExists_indexExists() { @@ -85,15 +81,8 @@ add_task(function* test_attach_createTable_tableExists_indexExists() { do_check_false(msc.tableExists("sample.test")); msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"); do_check_true(msc.tableExists("sample.test")); - try { - msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"); - do_throw("We shouldn't get here!"); - } catch (e) { - if (e.result != Components.results.NS_ERROR_FAILURE) { - throw e; - } - // we expect to fail because this table should exist already. - } + Assert.throws(() => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/); do_check_false(msc.indexExists("sample.test_ind")); msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)"); @@ -135,23 +124,13 @@ add_task(function* test_transactionInProgress_yes() { add_task(function* test_commitTransaction_no_transaction() { var msc = getOpenedDatabase(); do_check_false(msc.transactionInProgress); - try { - msc.commitTransaction(); - do_throw("We should not get here!"); - } catch (e) { - do_check_eq(Cr.NS_ERROR_UNEXPECTED, e.result); - } + Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/); }); add_task(function* test_rollbackTransaction_no_transaction() { var msc = getOpenedDatabase(); do_check_false(msc.transactionInProgress); - try { - msc.rollbackTransaction(); - do_throw("We should not get here!"); - } catch (e) { - do_check_eq(Cr.NS_ERROR_UNEXPECTED, e.result); - } + Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/); }); add_task(function* test_get_schemaVersion_not_set() { @@ -292,21 +271,15 @@ add_task(function* test_close_fails_with_async_statement_ran() { stmt.finalize(); let db = getOpenedDatabase(); - try { - db.close(); - do_throw("should have thrown"); - } - catch (e) { - do_check_eq(e.result, Cr.NS_ERROR_UNEXPECTED); - } - finally { - // Clean up after ourselves. - db.asyncClose(function () { - // Reset gDBConn so that later tests will get a new connection object. - gDBConn = null; - deferred.resolve(); - }); - } + Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/); + + // Clean up after ourselves. + db.asyncClose(function () { + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + deferred.resolve(); + }); + yield deferred.promise; }); diff --git a/storage/test/unit/test_storage_value_array.js b/storage/test/unit/test_storage_value_array.js index dbeb7bb56a1..27bd23992e3 100644 --- a/storage/test/unit/test_storage_value_array.js +++ b/storage/test/unit/test_storage_value_array.js @@ -4,8 +4,7 @@ // This file tests the functions of mozIStorageValueArray -function setup() -{ +add_task(function* setup() { getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT," + "number REAL, nuller NULL, blobber BLOB"); @@ -23,10 +22,11 @@ function setup() stmt.reset(); stmt.finalize(); -} -function test_getIsNull_for_null() -{ + do_register_cleanup(cleanup); +}); + +add_task(function* test_getIsNull_for_null() { var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1"); stmt.bindByIndex(0, 1); do_check_true(stmt.executeStep()); @@ -35,10 +35,9 @@ function test_getIsNull_for_null() do_check_true(stmt.getIsNull(1)); // data is null if size is 0 stmt.reset(); stmt.finalize(); -} +}); -function test_getIsNull_for_non_null() -{ +add_task(function* test_getIsNull_for_non_null() { var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1"); stmt.bindByIndex(0, 2); do_check_true(stmt.executeStep()); @@ -47,10 +46,9 @@ function test_getIsNull_for_non_null() do_check_false(stmt.getIsNull(1)); stmt.reset(); stmt.finalize(); -} +}); -function test_value_type_null() -{ +add_task(function* test_value_type_null() { var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1"); stmt.bindByIndex(0, 1); do_check_true(stmt.executeStep()); @@ -59,10 +57,9 @@ function test_value_type_null() stmt.getTypeOfIndex(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_value_type_integer() -{ +add_task(function* test_value_type_integer() { var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); stmt.bindByIndex(0, 1); do_check_true(stmt.executeStep()); @@ -71,10 +68,9 @@ function test_value_type_integer() stmt.getTypeOfIndex(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_value_type_float() -{ +add_task(function* test_value_type_float() { var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); stmt.bindByIndex(0, 1); do_check_true(stmt.executeStep()); @@ -83,10 +79,9 @@ function test_value_type_float() stmt.getTypeOfIndex(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_value_type_text() -{ +add_task(function* test_value_type_text() { var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); stmt.bindByIndex(0, 1); do_check_true(stmt.executeStep()); @@ -95,10 +90,9 @@ function test_value_type_text() stmt.getTypeOfIndex(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_value_type_blob() -{ +add_task(function* test_value_type_blob() { var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); stmt.bindByIndex(0, 2); do_check_true(stmt.executeStep()); @@ -107,10 +101,9 @@ function test_value_type_blob() stmt.getTypeOfIndex(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_numEntries_one() -{ +add_task(function* test_numEntries_one() { var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); stmt.bindByIndex(0, 2); do_check_true(stmt.executeStep()); @@ -118,10 +111,9 @@ function test_numEntries_one() do_check_eq(1, stmt.numEntries); stmt.reset(); stmt.finalize(); -} +}); -function test_numEntries_all() -{ +add_task(function* test_numEntries_all() { var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); stmt.bindByIndex(0, 2); do_check_true(stmt.executeStep()); @@ -129,10 +121,9 @@ function test_numEntries_all() do_check_eq(5, stmt.numEntries); stmt.reset(); stmt.finalize(); -} +}); -function test_getInt() -{ +add_task(function* test_getInt() { var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); stmt.bindByIndex(0, 2); do_check_true(stmt.executeStep()); @@ -141,10 +132,9 @@ function test_getInt() do_check_eq(2, stmt.getInt64(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_getDouble() -{ +add_task(function* test_getDouble() { var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); stmt.bindByIndex(0, 2); do_check_true(stmt.executeStep()); @@ -152,10 +142,9 @@ function test_getDouble() do_check_eq(1.23, stmt.getDouble(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_getUTF8String() -{ +add_task(function* test_getUTF8String() { var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); stmt.bindByIndex(0, 1); do_check_true(stmt.executeStep()); @@ -163,10 +152,9 @@ function test_getUTF8String() do_check_eq("foo", stmt.getUTF8String(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_getString() -{ +add_task(function* test_getString() { var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); stmt.bindByIndex(0, 2); do_check_true(stmt.executeStep()); @@ -174,10 +162,9 @@ function test_getString() do_check_eq("", stmt.getString(0)); stmt.reset(); stmt.finalize(); -} +}); -function test_getBlob() -{ +add_task(function* test_getBlob() { var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); stmt.bindByIndex(0, 2); do_check_true(stmt.executeStep()); @@ -190,22 +177,6 @@ function test_getBlob() do_check_eq(2, arr.value[1]); stmt.reset(); stmt.finalize(); -} +}); -var tests = [test_getIsNull_for_null, test_getIsNull_for_non_null, - test_value_type_null, test_value_type_integer, - test_value_type_float, test_value_type_text, test_value_type_blob, - test_numEntries_one, test_numEntries_all, test_getInt, - test_getDouble, test_getUTF8String, test_getString, test_getBlob]; - -function run_test() -{ - setup(); - - for (var i = 0; i < tests.length; i++) { - tests[i](); - } - - cleanup(); -} diff --git a/storage/test/unit/test_unicode.js b/storage/test/unit/test_unicode.js index 8aa8f012960..7753bbfdbf2 100644 --- a/storage/test/unit/test_unicode.js +++ b/storage/test/unit/test_unicode.js @@ -7,8 +7,7 @@ const LATIN1_AE = "\xc6"; // "Æ" const LATIN1_ae = "\xe6"; // "æ" -function setup() -{ +add_task(function* setup() { getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)"); @@ -25,20 +24,20 @@ function setup() stmt.bindByIndex(1, 4); stmt.execute(); stmt.finalize(); -} -function test_upper_ascii() -{ + do_register_cleanup(cleanup); +}); + +add_task(function* test_upper_ascii() { var stmt = createStatement("SELECT name, id FROM test WHERE name = upper('a')"); do_check_true(stmt.executeStep()); do_check_eq("A", stmt.getString(0)); do_check_eq(2, stmt.getInt32(1)); stmt.reset(); stmt.finalize(); -} +}); -function test_upper_non_ascii() -{ +add_task(function* test_upper_non_ascii() { var stmt = createStatement("SELECT name, id FROM test WHERE name = upper(?1)"); stmt.bindByIndex(0, LATIN1_ae); do_check_true(stmt.executeStep()); @@ -46,20 +45,18 @@ function test_upper_non_ascii() do_check_eq(1, stmt.getInt32(1)); stmt.reset(); stmt.finalize(); -} +}); -function test_lower_ascii() -{ +add_task(function* test_lower_ascii() { var stmt = createStatement("SELECT name, id FROM test WHERE name = lower('B')"); do_check_true(stmt.executeStep()); do_check_eq("b", stmt.getString(0)); do_check_eq(3, stmt.getInt32(1)); stmt.reset(); stmt.finalize(); -} +}); -function test_lower_non_ascii() -{ +add_task(function* test_lower_non_ascii() { var stmt = createStatement("SELECT name, id FROM test WHERE name = lower(?1)"); stmt.bindByIndex(0, LATIN1_AE); do_check_true(stmt.executeStep()); @@ -67,38 +64,20 @@ function test_lower_non_ascii() do_check_eq(4, stmt.getInt32(1)); stmt.reset(); stmt.finalize(); -} +}); -function test_like_search_different() -{ +add_task(function* test_like_search_different() { var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); stmt.bindByIndex(0, LATIN1_AE); do_check_true(stmt.executeStep()); do_check_eq(2, stmt.getInt32(0)); stmt.finalize(); -} +}); -function test_like_search_same() -{ +add_task(function* test_like_search_same() { var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); stmt.bindByIndex(0, LATIN1_ae); do_check_true(stmt.executeStep()); do_check_eq(2, stmt.getInt32(0)); stmt.finalize(); -} - -var tests = [test_upper_ascii, test_upper_non_ascii, test_lower_ascii, - test_lower_non_ascii, test_like_search_different, - test_like_search_same]; - -function run_test() -{ - setup(); - - for (var i = 0; i < tests.length; i++) { - tests[i](); - } - - cleanup(); -} - +}); diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index e0d59db4354..8c080bb5df5 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -52,6 +52,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", "resource://gre/modules/MessageChannel.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", + "resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/ExtensionManagement.jsm"); @@ -311,6 +313,21 @@ ExtensionPage = class extends BaseContext { } }; +// For extensions that have called setUninstallURL(), send an event +// so the browser can display the URL. +let UninstallObserver = { + init: function() { + AddonManager.addAddonListener(this); + }, + + onUninstalling: function(addon) { + let extension = GlobalManager.extensionMap.get(addon.id); + if (extension) { + Management.emit("uninstall", extension); + } + }, +}; + // Responsible for loading extension APIs into the right globals. GlobalManager = { // Number of extensions currently enabled. @@ -326,6 +343,7 @@ GlobalManager = { init(extension) { if (this.count == 0) { Services.obs.addObserver(this, "content-document-global-created", false); + UninstallObserver.init(); } this.count++; @@ -365,6 +383,10 @@ GlobalManager = { let schemaApi = Management.generateAPIs(extension, context, Management.schemaApis); let schemaWrapper = { + get cloneScope() { + return context.cloneScope; + }, + callFunction(ns, name, args) { return schemaApi[ns][name](...args); }, @@ -379,22 +401,17 @@ GlobalManager = { let promise; try { - // TODO: Stop passing the callback once all APIs return - // promises. - promise = schemaApi[ns][name](...args, callback); + promise = schemaApi[ns][name](...args); } catch (e) { - promise = Promise.reject(e); - // TODO: Certain tests are still expecting API methods to - // throw errors. - throw e; + if (e instanceof context.cloneScope.Error) { + promise = Promise.reject(e); + } else { + Cu.reportError(e); + promise = Promise.reject({ message: "An unexpected error occurred" }); + } } - // TODO: This check should no longer be necessary - // once all async methods return promises. - if (promise) { - return context.wrapPromise(promise, callback); - } - return undefined; + return context.wrapPromise(promise || Promise.resolve(), callback); }, getProperty(ns, name) { @@ -605,6 +622,10 @@ ExtensionData.prototype = { url: this.baseURI && this.baseURI.spec, principal: this.principal, + + logError: error => { + this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`); + }, }; let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context); @@ -806,6 +827,8 @@ this.Extension = function(addonData) { this.hasShutdown = false; this.onShutdown = new Set(); + this.uninstallURL = null; + this.permissions = new Set(); this.whiteListedHosts = null; this.webAccessibleResources = new Set(); diff --git a/toolkit/components/extensions/Schemas.jsm b/toolkit/components/extensions/Schemas.jsm index a7ccf812a84..4894cdde2ed 100644 --- a/toolkit/components/extensions/Schemas.jsm +++ b/toolkit/components/extensions/Schemas.jsm @@ -94,6 +94,13 @@ class Context { if ("checkLoadURL" in params) { this.checkLoadURL = params.checkLoadURL; } + if ("logError" in params) { + this.logError = params.logError; + } + } + + get cloneScope() { + return this.params.cloneScope; } get url() { @@ -115,6 +122,13 @@ class Context { return true; } + /** + * Returns an error result object with the given message, for return + * by Type normalization functions. + * + * If the context has a `currentTarget` value, this is prepended to + * the message to indicate the location of the error. + */ error(message) { if (this.currentTarget) { return {error: `Error processing ${this.currentTarget}: ${message}`}; @@ -122,10 +136,52 @@ class Context { return {error: message}; } + /** + * Creates an `Error` object belonging to the current unprivileged + * scope. If there is no unprivileged scope associated with this + * context, the message is returned as a string. + * + * If the context has a `currentTarget` value, this is prepended to + * the message, in the same way as for the `error` method. + */ + makeError(message) { + let {error} = this.error(message); + if (this.cloneScope) { + return new this.cloneScope.Error(error); + } + return error; + } + + /** + * Logs the given error to the console. May be overridden to enable + * custom logging. + */ + logError(error) { + Cu.reportError(error); + } + + /** + * Returns the name of the value currently being normalized. For a + * nested object, this is usually approximately equivalent to the + * JavaScript property accessor for that property. Given: + * + * { foo: { bar: [{ baz: x }] } } + * + * When processing the value for `x`, the currentTarget is + * 'foo.bar.0.baz' + */ get currentTarget() { return this.path.join("."); } + /** + * Appends the given component to the `currentTarget` path to indicate + * that it is being processed, calls the given callback function, and + * then restores the original path. + * + * This is used to identify the path of the property being processed + * when reporting type errors. + */ withPath(component, callback) { this.path.push(component); try { @@ -193,12 +249,63 @@ const FORMATS = { // properties, functions, and events. An Entry is a base class for // types, properties, functions, and events. class Entry { + constructor(schema = {}) { + /** + * If set to any value which evaluates as true, this entry is + * deprecated, and any access to it will result in a deprecation + * warning being logged to the browser console. + * + * If the value is a string, it will be appended to the deprecation + * message. If it contains the substring "${value}", it will be + * replaced with a string representation of the value being + * processed. + * + * If the value is any other truthy value, a generic deprecation + * message will be emitted. + */ + this.deprecated = false; + if ("deprecated" in schema) { + this.deprecated = schema.deprecated; + } + } + + /** + * Logs a deprecation warning for this entry, based on the value of + * its `deprecated` property. + */ + logDeprecation(context, value = null) { + let message = "This property is deprecated"; + if (typeof(this.deprecated) == "string") { + message = this.deprecated; + if (message.includes("${value}")) { + try { + value = JSON.stringify(value); + } catch (e) { + value = String(value); + } + message = message.replace(/\$\{value\}/g, () => value); + } + } + + context.logError(context.makeError(message)); + } + + /** + * Checks whether the entry is deprecated and, if so, logs a + * deprecation message. + */ + checkDeprecated(context, value = null) { + if (this.deprecated) { + this.logDeprecation(context, value); + } + } + // Injects JS values for the entry into the extension API // namespace. The default implementation is to do - // nothing. |wrapperFuncs| is used to call the actual implementation + // nothing. |context| is used to call the actual implementation // of a given function or event. It's an object with properties // callFunction, addListener, removeListener, and hasListener. - inject(name, dest, wrapperFuncs) { + inject(name, dest, context) { } } @@ -227,6 +334,7 @@ class Type extends Entry { // normalize. Subclasses can choose to use it or not. normalizeBase(type, value, context) { if (this.checkBaseType(getValueBaseType(value))) { + this.checkDeprecated(context, value); return {value}; } return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`); @@ -235,7 +343,8 @@ class Type extends Entry { // Type that allows any value. class AnyType extends Type { - normalize(value) { + normalize(value, context) { + this.checkDeprecated(context, value); return {value}; } @@ -246,8 +355,8 @@ class AnyType extends Type { // An untagged union type. class ChoiceType extends Type { - constructor(choices) { - super(); + constructor(schema, choices) { + super(schema); this.choices = choices; } @@ -258,6 +367,8 @@ class ChoiceType extends Type { } normalize(value, context) { + this.checkDeprecated(context, value); + let error; let baseType = getValueBaseType(value); @@ -283,8 +394,8 @@ class ChoiceType extends Type { class RefType extends Type { // For a reference to a type named T declared in namespace NS, // namespaceName will be NS and reference will be T. - constructor(namespaceName, reference) { - super(); + constructor(schema, namespaceName, reference) { + super(schema); this.namespaceName = namespaceName; this.reference = reference; } @@ -299,6 +410,7 @@ class RefType extends Type { } normalize(value, context) { + this.checkDeprecated(context, value); return this.targetType.normalize(value, context); } @@ -308,8 +420,8 @@ class RefType extends Type { } class StringType extends Type { - constructor(enumeration, minLength, maxLength, pattern, format) { - super(); + constructor(schema, enumeration, minLength, maxLength, pattern, format) { + super(schema); this.enumeration = enumeration; this.minLength = minLength; this.maxLength = maxLength; @@ -356,7 +468,7 @@ class StringType extends Type { return baseType == "string"; } - inject(name, dest, wrapperFuncs) { + inject(name, dest, context) { if (this.enumeration) { let obj = Cu.createObjectIn(dest, {defineAs: name}); for (let e of this.enumeration) { @@ -368,8 +480,8 @@ class StringType extends Type { } class ObjectType extends Type { - constructor(properties, additionalProperties, patternProperties, isInstanceOf) { - super(); + constructor(schema, properties, additionalProperties, patternProperties, isInstanceOf) { + super(schema); this.properties = properties; this.additionalProperties = additionalProperties; this.patternProperties = patternProperties; @@ -531,8 +643,8 @@ class NumberType extends Type { } class IntegerType extends Type { - constructor(minimum, maximum) { - super(); + constructor(schema, minimum, maximum) { + super(schema); this.minimum = minimum; this.maximum = maximum; } @@ -574,8 +686,8 @@ class BooleanType extends Type { } class ArrayType extends Type { - constructor(itemType, minItems, maxItems) { - super(); + constructor(schema, itemType, minItems, maxItems) { + super(schema); this.itemType = itemType; this.minItems = minItems; this.maxItems = maxItems; @@ -613,8 +725,8 @@ class ArrayType extends Type { } class FunctionType extends Type { - constructor(parameters, isAsync) { - super(); + constructor(schema, parameters, isAsync) { + super(schema); this.parameters = parameters; this.isAsync = isAsync; } @@ -631,13 +743,13 @@ class FunctionType extends Type { // Represents a "property" defined in a schema namespace with a // particular value. Essentially this is a constant. class ValueProperty extends Entry { - constructor(name, value) { - super(); + constructor(schema, name, value) { + super(schema); this.name = name; this.value = value; } - inject(name, dest, wrapperFuncs) { + inject(name, dest, context) { dest[name] = this.value; } } @@ -645,26 +757,26 @@ class ValueProperty extends Entry { // Represents a "property" defined in a schema namespace that is not a // constant. class TypeProperty extends Entry { - constructor(namespaceName, name, type, writable) { - super(); + constructor(schema, namespaceName, name, type, writable) { + super(schema); this.namespaceName = namespaceName; this.name = name; this.type = type; this.writable = writable; } - throwError(global, msg) { - global = Cu.getGlobalForObject(global); - throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`); + throwError(context, msg) { + throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`); } - inject(name, dest, wrapperFuncs) { + inject(name, dest, context) { if (this.unsupported) { return; } let getStub = () => { - return wrapperFuncs.getProperty(this.namespaceName, name); + this.checkDeprecated(context); + return context.getProperty(this.namespaceName, name); }; let desc = { @@ -676,12 +788,12 @@ class TypeProperty extends Entry { if (this.writable) { let setStub = (value) => { - let normalized = this.type.normalize(value); + let normalized = this.type.normalize(value, context); if (normalized.error) { - this.throwError(dest, normalized.error); + this.throwError(context, normalized.error); } - wrapperFuncs.setProperty(this.namespaceName, name, normalized.value); + context.setProperty(this.namespaceName, name, normalized.value); }; desc.set = Cu.exportFunction(setStub, dest); @@ -695,20 +807,19 @@ class TypeProperty extends Entry { // care of validating parameter lists (i.e., handling of optional // parameters and parameter type checking). class CallEntry extends Entry { - constructor(namespaceName, name, parameters, allowAmbiguousOptionalArguments) { - super(); + constructor(schema, namespaceName, name, parameters, allowAmbiguousOptionalArguments) { + super(schema); this.namespaceName = namespaceName; this.name = name; this.parameters = parameters; this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments; } - throwError(global, msg) { - global = Cu.getGlobalForObject(global); - throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`); + throwError(context, msg) { + throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`); } - checkParameters(args, global, context) { + checkParameters(args, context) { let fixedArgs = []; // First we create a new array, fixedArgs, that is the same as @@ -756,7 +867,7 @@ class CallEntry extends Entry { } else { let success = check(0, 0); if (!success) { - this.throwError(global, "Incorrect argument types"); + this.throwError(context, "Incorrect argument types"); } } @@ -768,7 +879,7 @@ class CallEntry extends Entry { let parameter = this.parameters[parameterIndex]; let r = parameter.type.normalize(arg, context); if (r.error) { - this.throwError(global, `Type error for parameter ${parameter.name} (${r.error})`); + this.throwError(context, `Type error for parameter ${parameter.name} (${r.error})`); } return r.value; } @@ -780,31 +891,32 @@ class CallEntry extends Entry { // Represents a "function" defined in a schema namespace. class FunctionEntry extends CallEntry { - constructor(namespaceName, name, type, unsupported, allowAmbiguousOptionalArguments, returns) { - super(namespaceName, name, type.parameters, allowAmbiguousOptionalArguments); + constructor(schema, namespaceName, name, type, unsupported, allowAmbiguousOptionalArguments, returns) { + super(schema, namespaceName, name, type.parameters, allowAmbiguousOptionalArguments); this.unsupported = unsupported; this.returns = returns; this.isAsync = type.isAsync; } - inject(name, dest, wrapperFuncs) { + inject(name, dest, context) { if (this.unsupported) { return; } - let context = new Context(wrapperFuncs); let stub; if (this.isAsync) { stub = (...args) => { - let actuals = this.checkParameters(args, dest, context); + this.checkDeprecated(context); + let actuals = this.checkParameters(args, context); let callback = actuals.pop(); - return wrapperFuncs.callAsyncFunction(this.namespaceName, name, actuals, callback); + return context.callAsyncFunction(this.namespaceName, name, actuals, callback); }; } else { stub = (...args) => { - let actuals = this.checkParameters(args, dest, context); - return wrapperFuncs.callFunction(this.namespaceName, name, actuals); + this.checkDeprecated(context); + let actuals = this.checkParameters(args, context); + return context.callFunction(this.namespaceName, name, actuals); }; } Cu.exportFunction(stub, dest, {defineAs: name}); @@ -813,41 +925,39 @@ class FunctionEntry extends CallEntry { // Represents an "event" defined in a schema namespace. class Event extends CallEntry { - constructor(namespaceName, name, type, extraParameters, unsupported) { - super(namespaceName, name, extraParameters); + constructor(schema, namespaceName, name, type, extraParameters, unsupported) { + super(schema, namespaceName, name, extraParameters); this.type = type; this.unsupported = unsupported; } - checkListener(global, listener, context) { + checkListener(listener, context) { let r = this.type.normalize(listener, context); if (r.error) { - this.throwError(global, "Invalid listener"); + this.throwError(context, "Invalid listener"); } return r.value; } - inject(name, dest, wrapperFuncs) { + inject(name, dest, context) { if (this.unsupported) { return; } - let context = new Context(wrapperFuncs); - let addStub = (listener, ...args) => { - listener = this.checkListener(dest, listener, context); - let actuals = this.checkParameters(args, dest, context); - return wrapperFuncs.addListener(this.namespaceName, name, listener, actuals); + listener = this.checkListener(listener, context); + let actuals = this.checkParameters(args, context); + return context.addListener(this.namespaceName, name, listener, actuals); }; let removeStub = (listener) => { - listener = this.checkListener(dest, listener, context); - return wrapperFuncs.removeListener(this.namespaceName, name, listener); + listener = this.checkListener(listener, context); + return context.removeListener(this.namespaceName, name, listener); }; let hasStub = (listener) => { - listener = this.checkListener(dest, listener, context); - return wrapperFuncs.hasListener(this.namespaceName, name, listener); + listener = this.checkListener(listener, context); + return context.hasListener(this.namespaceName, name, listener); }; let obj = Cu.createObjectIn(dest, {defineAs: name}); @@ -876,7 +986,7 @@ this.Schemas = { // Do some simple validation of our own schemas. function checkTypeProperties(...extra) { - let allowedSet = new Set([...allowedProperties, ...extra, "description"]); + let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated"]); for (let prop of Object.keys(type)) { if (!allowedSet.has(prop)) { throw new Error(`Internal error: Namespace ${namespaceName} has invalid type property "${prop}" in type "${type.id || JSON.stringify(type)}"`); @@ -888,7 +998,7 @@ this.Schemas = { checkTypeProperties("choices"); let choices = type.choices.map(t => this.parseType(namespaceName, t)); - return new ChoiceType(choices); + return new ChoiceType(type, choices); } else if ("$ref" in type) { checkTypeProperties("$ref"); let ref = type.$ref; @@ -896,7 +1006,7 @@ this.Schemas = { if (ref.includes(".")) { [ns, ref] = ref.split("."); } - return new RefType(ns, ref); + return new RefType(type, ns, ref); } if (!("type" in type)) { @@ -939,7 +1049,7 @@ this.Schemas = { } format = FORMATS[type.format]; } - return new StringType(enumeration, + return new StringType(type, enumeration, type.minLength || 0, type.maxLength || Infinity, pattern, @@ -948,7 +1058,7 @@ this.Schemas = { let parseProperty = (type, extraProps = []) => { return { type: this.parseType(namespaceName, type, - ["unsupported", "deprecated", ...extraProps]), + ["unsupported", ...extraProps]), optional: type.optional || false, unsupported: type.unsupported || false, }; @@ -985,20 +1095,20 @@ this.Schemas = { } else { checkTypeProperties("properties", "additionalProperties", "patternProperties", "isInstanceOf"); } - return new ObjectType(properties, additionalProperties, patternProperties, type.isInstanceOf || null); + return new ObjectType(type, properties, additionalProperties, patternProperties, type.isInstanceOf || null); } else if (type.type == "array") { checkTypeProperties("items", "minItems", "maxItems"); - return new ArrayType(this.parseType(namespaceName, type.items), + return new ArrayType(type, this.parseType(namespaceName, type.items), type.minItems || 0, type.maxItems || Infinity); } else if (type.type == "number") { checkTypeProperties(); - return new NumberType(); + return new NumberType(type); } else if (type.type == "integer") { checkTypeProperties("minimum", "maximum"); - return new IntegerType(type.minimum || 0, type.maximum || Infinity); + return new IntegerType(type, type.minimum || 0, type.maximum || Infinity); } else if (type.type == "boolean") { checkTypeProperties(); - return new BooleanType(); + return new BooleanType(type); } else if (type.type == "function") { let isAsync = typeof(type.async) == "string"; @@ -1028,11 +1138,11 @@ this.Schemas = { } checkTypeProperties("parameters", "async", "returns"); - return new FunctionType(parameters, isAsync); + return new FunctionType(type, parameters, isAsync); } else if (type.type == "any") { // Need to see what minimum and maximum are supposed to do here. checkTypeProperties("minimum", "maximum"); - return new AnyType(); + return new AnyType(type); } else { throw new Error(`Unexpected type ${type.type}`); } @@ -1069,20 +1179,19 @@ this.Schemas = { loadProperty(namespaceName, name, prop) { if ("value" in prop) { - this.register(namespaceName, name, new ValueProperty(name, prop.value)); + this.register(namespaceName, name, new ValueProperty(prop, name, prop.value)); } else { // We ignore the "optional" attribute on properties since we // don't inject anything here anyway. let type = this.parseType(namespaceName, prop, ["optional", "writable"]); - this.register(namespaceName, name, new TypeProperty(namespaceName, name, type), - prop.writable); + this.register(namespaceName, name, new TypeProperty(prop, namespaceName, name, type, prop.writable || false)); } }, loadFunction(namespaceName, fun) { - let f = new FunctionEntry(namespaceName, fun.name, + let f = new FunctionEntry(fun, namespaceName, fun.name, this.parseType(namespaceName, fun, - ["name", "unsupported", "deprecated", "returns", + ["name", "unsupported", "returns", "allowAmbiguousOptionalArguments"]), fun.unsupported || false, fun.allowAmbiguousOptionalArguments || false, @@ -1107,10 +1216,10 @@ this.Schemas = { /* eslint-enable no-unused-vars */ let type = this.parseType(namespaceName, event, - ["name", "unsupported", "deprecated", + ["name", "unsupported", "extraParameters", "returns", "filters"]); - let e = new Event(namespaceName, event.name, type, extras, + let e = new Event(event, namespaceName, event.name, type, extras, event.unsupported || false); this.register(namespaceName, event.name, e); }, diff --git a/toolkit/components/extensions/ext-runtime.js b/toolkit/components/extensions/ext-runtime.js index 1173ae368c9..c22a72c7edf 100644 --- a/toolkit/components/extensions/ext-runtime.js +++ b/toolkit/components/extensions/ext-runtime.js @@ -85,6 +85,26 @@ extensions.registerSchemaAPI("runtime", null, (extension, context) => { let info = {os, arch}; return Promise.resolve(info); }, + + setUninstallURL: function(url) { + if (url.length == 0) { + return Promise.resolve(); + } + + let uri; + try { + uri = NetUtil.newURI(url); + } catch (e) { + return Promise.reject({ message: `Invalid URL: ${JSON.stringify(url)}` }); + } + + if (uri.scheme != "http" && uri.scheme != "https") { + return Promise.reject({ message: "url must have the scheme http or https" }); + } + + extension.uninstallURL = url; + return Promise.resolve(); + }, }, }; }); diff --git a/toolkit/components/extensions/schemas/manifest.json b/toolkit/components/extensions/schemas/manifest.json index 71c06048c23..9812f443e52 100644 --- a/toolkit/components/extensions/schemas/manifest.json +++ b/toolkit/components/extensions/schemas/manifest.json @@ -102,7 +102,10 @@ "items": { "choices": [ { "$ref": "Permission" }, - { "type": "string" } + { + "type": "string", + "deprecated": "Unknown permission ${value}" + } ] }, "optional": true @@ -116,7 +119,8 @@ }, "additionalProperties": { - "type": "any" + "type": "any", + "deprecated": "An unexpected property was found in the WebExtension manifest" } }, { diff --git a/toolkit/components/extensions/schemas/runtime.json b/toolkit/components/extensions/schemas/runtime.json index 0c8f3e1e9b7..78513112829 100644 --- a/toolkit/components/extensions/schemas/runtime.json +++ b/toolkit/components/extensions/schemas/runtime.json @@ -175,7 +175,6 @@ }, { "name": "setUninstallURL", - "unsupported": true, "type": "function", "description": "Sets the URL to be visited upon uninstallation. This may be used to clean up server-side data, do analytics, and implement surveys. Maximum 255 characters.", "async": "callback", diff --git a/toolkit/components/extensions/test/mochitest/mochitest.ini b/toolkit/components/extensions/test/mochitest/mochitest.ini index 38efe5cb68c..8ba4780083b 100644 --- a/toolkit/components/extensions/test/mochitest/mochitest.ini +++ b/toolkit/components/extensions/test/mochitest/mochitest.ini @@ -25,6 +25,7 @@ support-files = [test_ext_simple.html] [test_ext_schema.html] +skip-if = e10s # Uses a console montitor. Actual code does not depend on e10s. [test_ext_geturl.html] [test_ext_contentscript.html] skip-if = buildapp == 'b2g' # runat != document_idle is not supported. diff --git a/toolkit/components/extensions/test/mochitest/test_ext_schema.html b/toolkit/components/extensions/test/mochitest/test_ext_schema.html index 066a2f6f748..a4ac4ea5ad4 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_schema.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html @@ -13,7 +13,7 @@ diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js index a9e157cec78..d994a8ae0c0 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js @@ -275,6 +275,18 @@ function verify(...args) { tallied = null; } +let talliedErrors = []; + +function checkErrors(errors) { + do_check_eq(talliedErrors.length, errors.length, "Got expected number of errors"); + for (let [i, error] of errors.entries()) { + do_check_true(i in talliedErrors && talliedErrors[i].includes(error), + `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`); + } + + talliedErrors.length = 0; +} + let wrapper = { url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/", @@ -282,10 +294,22 @@ let wrapper = { return !url.startsWith("chrome:"); }, + logError(message) { + talliedErrors.push(message); + }, + callFunction(ns, name, args) { tally("call", ns, name, args); }, + getProperty(ns, name) { + tally("get", ns, name); + }, + + setProperty(ns, name, value) { + tally("set", ns, name, value); + }, + addListener(ns, name, listener, args) { tally("addListener", ns, name, [listener, args]); }, @@ -565,3 +589,170 @@ add_task(function* () { /Incorrect argument types/, "should throw for wrong argument type"); }); + +let deprecatedJson = [ + {namespace: "deprecated", + + properties: { + accessor: { + type: "string", + writable: true, + deprecated: "This is not the property you are looking for", + }, + }, + + types: [ + { + "id": "Type", + "type": "string", + }, + ], + + functions: [ + { + name: "property", + type: "function", + parameters: [ + { + name: "arg", + type: "object", + properties: { + foo: { + type: "string", + }, + }, + additionalProperties: { + type: "any", + deprecated: "Unknown property", + }, + }, + ], + }, + + { + name: "value", + type: "function", + parameters: [ + { + name: "arg", + choices: [ + { + type: "integer", + }, + { + type: "string", + deprecated: "Please use an integer, not ${value}", + }, + ], + }, + ], + }, + + { + name: "choices", + type: "function", + parameters: [ + { + name: "arg", + deprecated: "You have no choices", + choices: [ + { + type: "integer", + }, + ], + }, + ], + }, + + { + name: "ref", + type: "function", + parameters: [ + { + name: "arg", + choices: [ + { + $ref: "Type", + deprecated: "Deprecated alias", + }, + ], + }, + ], + }, + + { + name: "method", + type: "function", + deprecated: "Do not call this method", + parameters: [ + ], + }, + ], + + events: [ + { + name: "onDeprecated", + type: "function", + deprecated: "This event does not work", + }, + ], + }, +]; + +add_task(function* testDeprecation() { + let url = "data:," + JSON.stringify(deprecatedJson); + let uri = BrowserUtils.makeURI(url); + yield Schemas.load(uri); + + let root = {}; + Schemas.inject(root, wrapper); + + talliedErrors.length = 0; + + + root.deprecated.property({foo: "bar", xxx: "any", yyy: "property"}); + verify("call", "deprecated", "property", [{foo: "bar", xxx: "any", yyy: "property"}]); + checkErrors([ + "Error processing xxx: Unknown property", + "Error processing yyy: Unknown property", + ]); + + root.deprecated.value(12); + verify("call", "deprecated", "value", [12]); + checkErrors([]); + + root.deprecated.value("12"); + verify("call", "deprecated", "value", ["12"]); + checkErrors(["Please use an integer, not \"12\""]); + + root.deprecated.choices(12); + verify("call", "deprecated", "choices", [12]); + checkErrors(["You have no choices"]); + + root.deprecated.ref("12"); + verify("call", "deprecated", "ref", ["12"]); + checkErrors(["Deprecated alias"]); + + root.deprecated.method(); + verify("call", "deprecated", "method", []); + checkErrors(["Do not call this method"]); + + + void root.deprecated.accessor; + verify("get", "deprecated", "accessor", null); + checkErrors(["This is not the property you are looking for"]); + + root.deprecated.accessor = "x"; + verify("set", "deprecated", "accessor", "x"); + checkErrors(["This is not the property you are looking for"]); + + + root.deprecated.onDeprecated.addListener(() => {}); + checkErrors(["This event does not work"]); + + root.deprecated.onDeprecated.removeListener(() => {}); + checkErrors(["This event does not work"]); + + root.deprecated.onDeprecated.hasListener(() => {}); + checkErrors(["This event does not work"]); +}); diff --git a/toolkit/components/places/nsPlacesAutoComplete.js b/toolkit/components/places/nsPlacesAutoComplete.js index abd9f725152..29bdae4c1a1 100644 --- a/toolkit/components/places/nsPlacesAutoComplete.js +++ b/toolkit/components/places/nsPlacesAutoComplete.js @@ -172,7 +172,7 @@ function stripPrefix(aURIString) } /** - * safePrefGetter get the pref with typo safety. + * safePrefGetter get the pref with type safety. * This will return the default value provided if no pref is set. * * @param aPrefBranch @@ -194,7 +194,11 @@ function safePrefGetter(aPrefBranch, aName, aDefault) { if (!type) { throw "Unknown type!"; } + // If the pref isn't set, we want to use the default. + if (aPrefBranch.getPrefType(aName) == Ci.nsIPrefBranch.PREF_INVALID) { + return aDefault; + } try { return aPrefBranch["get" + type + "Pref"](aName); } diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index a4549d000b0..0fa605e3fbc 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -8349,14 +8349,6 @@ "releaseChannelCollection": "opt-out", "description": "Connection length for bi-directionally connected media (0=SHORTER_THAN_10S, 1=BETWEEN_10S_AND_30S, 2=BETWEEN_30S_AND_5M, 3=MORE_THAN_5M)" }, - "LOOP_SHARING_STATE_CHANGE_1": { - "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"], - "expires_in_version": "48", - "kind": "enumerated", - "n_values": 8, - "releaseChannelCollection": "opt-out", - "description": "Number of times the sharing feature has been enabled and disabled (0=WINDOW_ENABLED, 1=WINDOW_DISABLED, 2=BROWSER_ENABLED, 3=BROWSER_DISABLED)" - }, "LOOP_SHARING_ROOM_URL": { "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"], "expires_in_version": "50", @@ -8381,21 +8373,6 @@ "releaseChannelCollection": "opt-out", "description": "Number of times a room delete action is performed (0=DELETE_SUCCESS, 2=DELETE_FAIL)" }, - "LOOP_ROOM_CONTEXT_ADD": { - "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"], - "expires_in_version": "48", - "kind": "enumerated", - "n_values": 8, - "releaseChannelCollection": "opt-out", - "description": "Number of times a room context action is performed (0=ADD_FROM_PANEL, 1=ADD_FROM_CONVERSATION)" - }, - "LOOP_ROOM_CONTEXT_CLICK": { - "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"], - "expires_in_version": "48", - "kind": "count", - "releaseChannelCollection": "opt-out", - "description": "Number times room context is clicked to visit the attached URL" - }, "LOOP_ROOM_SESSION_WITHCHAT": { "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"], "expires_in_version": "50", diff --git a/toolkit/components/telemetry/TelemetryEnvironment.jsm b/toolkit/components/telemetry/TelemetryEnvironment.jsm index 1d6441a5328..b32f0eea8e3 100644 --- a/toolkit/components/telemetry/TelemetryEnvironment.jsm +++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm @@ -1198,7 +1198,7 @@ EnvironmentCache.prototype = { * not a portable device. */ _getDeviceData: function () { - if (["gonk", "android"].indexOf(AppConstants.platform) === -1) { + if (!["gonk", "android"].includes(AppConstants.platform)) { return null; } @@ -1221,7 +1221,7 @@ EnvironmentCache.prototype = { locale: getSystemLocale(), }; - if (["gonk", "android"].indexOf(AppConstants.platform) !== -1) { + if (["gonk", "android"].includes(AppConstants.platform)) { data.kernelVersion = getSysinfoProperty("kernel_version", null); } else if (AppConstants.platform === "win") { let servicePack = getServicePack(); @@ -1270,7 +1270,7 @@ EnvironmentCache.prototype = { features: {}, }; - if (["gonk", "android", "linux"].indexOf(AppConstants.platform) === -1) { + if (!["gonk", "android", "linux"].includes(AppConstants.platform)) { let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); try { gfxData.monitors = gfxInfo.getMonitors(); @@ -1335,7 +1335,7 @@ EnvironmentCache.prototype = { if (AppConstants.platform === "win") { data.isWow64 = getSysinfoProperty("isWow64", null); - } else if (["gonk", "android"].indexOf(AppConstants.platform) !== -1) { + } else if (["gonk", "android"].includes(AppConstants.platform)) { data.device = this._getDeviceData(); } diff --git a/toolkit/components/telemetry/TelemetrySession.jsm b/toolkit/components/telemetry/TelemetrySession.jsm index 5b18982c040..b983dc2ef91 100644 --- a/toolkit/components/telemetry/TelemetrySession.jsm +++ b/toolkit/components/telemetry/TelemetrySession.jsm @@ -1276,7 +1276,7 @@ var Impl = { getSessionPayload: function getSessionPayload(reason, clearSubsession) { this._log.trace("getSessionPayload - reason: " + reason + ", clearSubsession: " + clearSubsession); - const isMobile = ["gonk", "android"].indexOf(AppConstants.platform) !== -1; + const isMobile = ["gonk", "android"].includes(AppConstants.platform); const isSubsession = isMobile ? false : !this._isClassicReason(reason); if (isMobile) { @@ -1998,7 +1998,7 @@ var Impl = { REASON_GATHER_PAYLOAD, REASON_TEST_PING, ]; - return classicReasons.indexOf(reason) != -1; + return classicReasons.includes(reason); }, /** diff --git a/toolkit/components/telemetry/histogram_tools.py b/toolkit/components/telemetry/histogram_tools.py index 89e687af6b0..2e5e5f670ab 100644 --- a/toolkit/components/telemetry/histogram_tools.py +++ b/toolkit/components/telemetry/histogram_tools.py @@ -271,7 +271,7 @@ associated with the histogram. Returns None if no guarding is necessary.""" if self._name not in n_buckets_whitelist: raise KeyError, ('New histogram %s is not permitted to have more than 100 buckets. ' 'Histograms with large numbers of buckets use disproportionately high amounts of resources. ' - 'Contact :vladan or the Perf team if you think an exception ought to be made.' % self._name) + 'Contact the Telemetry team (e.g. in #telemetry) if you think an exception ought to be made.' % self._name) @staticmethod def boolean_flag_bucket_parameters(definition): diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js index 32869eece3a..33332d6f4f0 100644 --- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js @@ -406,7 +406,7 @@ function checkPartnerSection(data, isInitial) { if (isInitial) { Assert.equal(data.partner.partnerNames.length, 0); } else { - Assert.ok(data.partner.partnerNames.indexOf(PARTNER_NAME) >= 0); + Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME)); } } diff --git a/toolkit/content/widgets/toolbarbutton.xml b/toolkit/content/widgets/toolbarbutton.xml index bb587fba0cd..5de3f040d21 100644 --- a/toolkit/content/widgets/toolbarbutton.xml +++ b/toolkit/content/widgets/toolbarbutton.xml @@ -87,7 +87,7 @@ - + @@ -102,7 +102,7 @@ - + diff --git a/toolkit/mozapps/installer/package-name.mk b/toolkit/mozapps/installer/package-name.mk index d791b529906..d3f4b194bfe 100644 --- a/toolkit/mozapps/installer/package-name.mk +++ b/toolkit/mozapps/installer/package-name.mk @@ -104,6 +104,12 @@ endif PKG_PATH = $(MOZ_PKG_PLATFORM)/$(AB_CD)/ CHECKSUMS_FILE_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION) MOZ_INFO_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION) +ifeq ($(MOZ_APP_NAME),xulrunner) +PKG_PATH = runtimes/ +PKG_BASENAME = $(MOZ_APP_NAME)-$(MOZ_PKG_VERSION).$(AB_CD).$(MOZ_PKG_PLATFORM) +CHECKSUMS_FILE_BASENAME = $(PKG_BASENAME) +MOZ_INFO_BASENAME = $(PKG_BASENAME) +endif PKG_INST_PATH = $(PKG_PATH) PKG_UPDATE_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION) PKG_UPDATE_PATH = update/$(PKG_PATH) diff --git a/toolkit/mozapps/installer/upload-files.mk b/toolkit/mozapps/installer/upload-files.mk index cbaa028ad46..9cfe53c11c3 100644 --- a/toolkit/mozapps/installer/upload-files.mk +++ b/toolkit/mozapps/installer/upload-files.mk @@ -51,7 +51,12 @@ PACKAGE = $(PKG_PATH)$(PKG_BASENAME)$(PKG_SUFFIX) # By default, the SDK uses the same packaging type as the main bundle, # but on mac it is a .tar.bz2 -SDK_PATH = $(PKG_PATH)/sdk/ +SDK_PATH = $(PKG_PATH) +ifeq ($(MOZ_APP_NAME),xulrunner) +SDK_PATH = sdk/ +# Don't codesign xulrunner internally +MOZ_INTERNAL_SIGNING_FORMAT = +endif SDK_SUFFIX = $(PKG_SUFFIX) SDK = $(SDK_PATH)$(PKG_BASENAME).sdk$(SDK_SUFFIX) ifdef UNIVERSAL_BINARY @@ -117,13 +122,13 @@ ifeq ($(MOZ_PKG_FORMAT),TAR) PKG_SUFFIX = .tar INNER_MAKE_PACKAGE = $(CREATE_FINAL_TAR) - $(MOZ_PKG_DIR) > $(PACKAGE) INNER_UNMAKE_PACKAGE = $(UNPACK_TAR) < $(UNPACKAGE) -MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk > '$(SDK)' +MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk > $(SDK) endif ifeq ($(MOZ_PKG_FORMAT),TGZ) PKG_SUFFIX = .tar.gz INNER_MAKE_PACKAGE = $(CREATE_FINAL_TAR) - $(MOZ_PKG_DIR) | gzip -vf9 > $(PACKAGE) INNER_UNMAKE_PACKAGE = gunzip -c $(UNPACKAGE) | $(UNPACK_TAR) -MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | gzip -vf9 > '$(SDK)' +MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | gzip -vf9 > $(SDK) endif ifeq ($(MOZ_PKG_FORMAT),BZ2) PKG_SUFFIX = .tar.bz2 @@ -133,7 +138,7 @@ else INNER_MAKE_PACKAGE = $(CREATE_FINAL_TAR) - $(MOZ_PKG_DIR) | bzip2 -vf > $(PACKAGE) endif INNER_UNMAKE_PACKAGE = bunzip2 -c $(UNPACKAGE) | $(UNPACK_TAR) -MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | bzip2 -vf > '$(SDK)' +MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | bzip2 -vf > $(SDK) endif ifeq ($(MOZ_PKG_FORMAT),ZIP) ifdef MOZ_EXTERNAL_SIGNING_FORMAT @@ -144,7 +149,7 @@ PKG_SUFFIX = .zip INNER_MAKE_PACKAGE = $(ZIP) -r9D $(PACKAGE) $(MOZ_PKG_DIR) \ -x \*/.mkdir.done INNER_UNMAKE_PACKAGE = $(UNZIP) $(UNPACKAGE) -MAKE_SDK = $(call py_action,zip,'$(SDK)' $(MOZ_APP_NAME)-sdk) +MAKE_SDK = $(call py_action,zip,$(SDK) $(MOZ_APP_NAME)-sdk) endif ifeq ($(MOZ_PKG_FORMAT),SFX7Z) PKG_SUFFIX = .exe @@ -550,7 +555,11 @@ INNER_UNMAKE_PACKAGE = \ # The plst and blkx resources are skipped because they belong to each # individual dmg and are created by hdiutil. SDK_SUFFIX = .tar.bz2 -MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | bzip2 -vf > '$(SDK)' +SDK = $(MOZ_PKG_APPNAME)-$(MOZ_PKG_VERSION).$(AB_CD).mac-$(TARGET_CPU).sdk$(SDK_SUFFIX) +ifeq ($(MOZ_APP_NAME),xulrunner) +SDK = $(SDK_PATH)$(MOZ_APP_NAME)-$(MOZ_PKG_VERSION).$(AB_CD).mac-$(TARGET_CPU).sdk$(SDK_SUFFIX) +endif +MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | bzip2 -vf > $(SDK) endif ifdef MOZ_INTERNAL_SIGNING_FORMAT @@ -586,7 +595,8 @@ MAKE_PACKAGE += && $(MOZ_SIGN_PACKAGE_CMD) '$(PACKAGE)' endif ifdef MOZ_SIGN_CMD -MAKE_SDK += && $(MOZ_SIGN_CMD) -f gpg '$(SDK)' +MAKE_SDK += && $(MOZ_SIGN_CMD) -f gpg $(SDK) +UPLOAD_EXTRA_FILES += $(SDK).asc endif NO_PKG_FILES += \ @@ -746,7 +756,6 @@ UPLOAD_FILES= \ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(GTEST_PACKAGE)) \ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(SYMBOL_ARCHIVE_BASENAME).zip) \ $(call QUOTED_WILDCARD,$(DIST)/$(SDK)) \ - $(call QUOTED_WILDCARD,$(DIST)/$(SDK).asc) \ $(call QUOTED_WILDCARD,$(MOZ_SOURCESTAMP_FILE)) \ $(call QUOTED_WILDCARD,$(MOZ_BUILDINFO_FILE)) \ $(call QUOTED_WILDCARD,$(MOZ_MOZINFO_FILE)) \ @@ -767,8 +776,8 @@ endif ifdef UNIFY_DIST UNIFY_ARCH := $(notdir $(patsubst %/,%,$(dir $(UNIFY_DIST)))) UPLOAD_FILES += \ - $(call QUOTED_WILDCARD,$(UNIFY_DIST)/$(SDK_PATH)$(PKG_BASENAME)-$(UNIFY_ARCH).sdk$(SDK_SUFFIX)) \ - $(call QUOTED_WILDCARD,$(UNIFY_DIST)/$(SDK_PATH)$(PKG_BASENAME)-$(UNIFY_ARCH).sdk$(SDK_SUFFIX).asc) + $(wildcard $(UNIFY_DIST)/$(SDK_PATH)$(PKG_BASENAME)-$(UNIFY_ARCH).sdk$(SDK_SUFFIX)) \ + $(wildcard $(UNIFY_DIST)/$(SDK_PATH)$(PKG_BASENAME)-$(UNIFY_ARCH).sdk$(SDK_SUFFIX).asc) endif SIGN_CHECKSUM_CMD=