diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /devtools/startup/DevToolsStartup.jsm | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | devtools/startup/DevToolsStartup.jsm | 1339 |
1 files changed, 1339 insertions, 0 deletions
diff --git a/devtools/startup/DevToolsStartup.jsm b/devtools/startup/DevToolsStartup.jsm new file mode 100644 index 0000000000..2c0525c041 --- /dev/null +++ b/devtools/startup/DevToolsStartup.jsm @@ -0,0 +1,1339 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This XPCOM component is loaded very early. + * Be careful to lazy load dependencies as much as possible. + * + * It manages all the possible entry points for DevTools: + * - Handles command line arguments like -jsconsole, + * - Register all key shortcuts, + * - Listen for "Web Developer" system menu opening, under "Tools", + * - Inject the wrench icon in toolbar customization, which is used + * by the "Web Developer" list displayed in the hamburger menu, + * - Register the JSON Viewer protocol handler. + * - Inject the profiler recording button in toolbar customization. + * + * Only once any of these entry point is fired, this module ensures starting + * core modules like 'devtools-browser.js' that hooks the browser windows + * and ensure setting up tools. + **/ + +"use strict"; + +const kDebuggerPrefs = [ + "devtools.debugger.remote-enabled", + "devtools.chrome.enabled", +]; + +const DEVTOOLS_ENABLED_PREF = "devtools.enabled"; +const DEVTOOLS_F12_DISABLED_PREF = "devtools.experiment.f12.shortcut_disabled"; + +const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled"; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "Services", + "resource://gre/modules/Services.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "CustomizableUI", + "resource:///modules/CustomizableUI.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "CustomizableWidgets", + "resource:///modules/CustomizableWidgets.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ProfilerMenuButton", + "resource://devtools/client/performance-new/popup/menu-button.jsm.js" +); +ChromeUtils.defineModuleGetter( + this, + "WebChannel", + "resource://gre/modules/WebChannel.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "PanelMultiView", + "resource:///modules/PanelMultiView.jsm" +); + +// We don't want to spend time initializing the full loader here so we create +// our own lazy require. +XPCOMUtils.defineLazyGetter(this, "Telemetry", function() { + const { require } = ChromeUtils.import( + "resource://devtools/shared/Loader.jsm" + ); + // eslint-disable-next-line no-shadow + const Telemetry = require("devtools/client/shared/telemetry"); + + return Telemetry; +}); + +XPCOMUtils.defineLazyGetter(this, "StartupBundle", function() { + const url = "chrome://devtools-startup/locale/startup.properties"; + return Services.strings.createBundle(url); +}); + +XPCOMUtils.defineLazyGetter(this, "KeyShortcutsBundle", function() { + const url = "chrome://devtools-startup/locale/key-shortcuts.properties"; + return Services.strings.createBundle(url); +}); + +/** + * Safely retrieve a localized DevTools key shortcut from KeyShortcutsBundle. + * If the shortcut is not available, this will return null. Consumer code + * should rely on this to skip unavailable shortcuts. + * + * Note that all shortcuts should always be available, but there is a notable + * exception, which is why we have to do this. When a localization change is + * uplifted to beta, language packs will not be updated immediately when the + * updated beta is available. + * + * This means that language pack users might get a new Beta version but will not + * have a language pack with the new strings yet. + */ +function getLocalizedKeyShortcut(id) { + try { + return KeyShortcutsBundle.GetStringFromName(id); + } catch (e) { + console.error("Failed to retrieve DevTools localized shortcut for id", id); + return null; + } +} + +XPCOMUtils.defineLazyGetter(this, "KeyShortcuts", function() { + const isMac = AppConstants.platform == "macosx"; + + // Common modifier shared by most key shortcuts + const modifiers = isMac ? "accel,alt" : "accel,shift"; + + // List of all key shortcuts triggering installation UI + // `id` should match tool's id from client/definitions.js + const shortcuts = [ + // The following keys are also registered in /client/menus.js + // And should be synced. + + // Both are toggling the toolbox on the last selected panel + // or the default one. + { + id: "toggleToolbox", + shortcut: getLocalizedKeyShortcut("toggleToolbox.commandkey"), + modifiers, + }, + // All locales are using F12 + { + id: "toggleToolboxF12", + shortcut: getLocalizedKeyShortcut("toggleToolboxF12.commandkey"), + modifiers: "", // F12 is the only one without modifiers + }, + // Open the Browser Toolbox + { + id: "browserToolbox", + shortcut: getLocalizedKeyShortcut("browserToolbox.commandkey"), + modifiers: "accel,alt,shift", + }, + // Open the Browser Console + { + id: "browserConsole", + shortcut: getLocalizedKeyShortcut("browserConsole.commandkey"), + modifiers: "accel,shift", + }, + // Toggle the Responsive Design Mode + { + id: "responsiveDesignMode", + shortcut: getLocalizedKeyShortcut("responsiveDesignMode.commandkey"), + modifiers, + }, + // The following keys are also registered in /client/definitions.js + // and should be synced. + + // Key for opening the Inspector + { + toolId: "inspector", + shortcut: getLocalizedKeyShortcut("inspector.commandkey"), + modifiers, + }, + // Key for opening the Web Console + { + toolId: "webconsole", + shortcut: getLocalizedKeyShortcut("webconsole.commandkey"), + modifiers, + }, + // Key for opening the Debugger + { + toolId: "jsdebugger", + shortcut: getLocalizedKeyShortcut("jsdebugger.commandkey2"), + modifiers, + }, + // Key for opening the Network Monitor + { + toolId: "netmonitor", + shortcut: getLocalizedKeyShortcut("netmonitor.commandkey"), + modifiers, + }, + // Key for opening the Style Editor + { + toolId: "styleeditor", + shortcut: getLocalizedKeyShortcut("styleeditor.commandkey"), + modifiers: "shift", + }, + // Key for opening the Performance Panel + { + toolId: "performance", + shortcut: getLocalizedKeyShortcut("performance.commandkey"), + modifiers: "shift", + }, + // Key for opening the Storage Panel + { + toolId: "storage", + shortcut: getLocalizedKeyShortcut("storage.commandkey"), + modifiers: "shift", + }, + // Key for opening the DOM Panel + { + toolId: "dom", + shortcut: getLocalizedKeyShortcut("dom.commandkey"), + modifiers, + }, + // Key for opening the Accessibility Panel + { + toolId: "accessibility", + shortcut: getLocalizedKeyShortcut("accessibilityF12.commandkey"), + modifiers: "shift", + }, + ]; + + if (isMac) { + // Add the extra key command for macOS, so you can open the inspector with cmd+shift+C + // like on Chrome DevTools. + shortcuts.push({ + id: "inspectorMac", + toolId: "inspector", + shortcut: getLocalizedKeyShortcut("inspector.commandkey"), + modifiers: "accel,shift", + }); + } + + if (ProfilerMenuButton.isInNavbar()) { + shortcuts.push(...getProfilerKeyShortcuts()); + } + + return shortcuts; +}); + +function getProfilerKeyShortcuts() { + return [ + // Start/stop the profiler + { + id: "profilerStartStop", + shortcut: getLocalizedKeyShortcut("profilerStartStop.commandkey"), + modifiers: "control,shift", + }, + // Capture a profile + { + id: "profilerCapture", + shortcut: getLocalizedKeyShortcut("profilerCapture.commandkey"), + modifiers: "control,shift", + }, + ]; +} + +/** + * Validate the URL that will be used for the WebChannel for the profiler. + * + * @param {string} targetUrl + * @returns {string} + */ +function validateProfilerWebChannelUrl(targetUrl) { + const frontEndUrl = "https://profiler.firefox.com"; + + if (targetUrl !== frontEndUrl) { + // The user can specify either localhost or deploy previews as well as + // the official frontend URL for testing. + if ( + // Allow a test URL. + targetUrl === "http://example.com" || + // Allows the following: + // "http://localhost:4242" + // "http://localhost:4242/" + // "http://localhost:3" + // "http://localhost:334798455" + /^http:\/\/localhost:\d+\/?$/.test(targetUrl) || + // Allows the following: + // "https://deploy-preview-1234--perf-html.netlify.com" + // "https://deploy-preview-1234--perf-html.netlify.com/" + // "https://deploy-preview-1234567--perf-html.netlify.app" + // "https://main--perf-html.netlify.app" + /^https:\/\/(?:deploy-preview-\d+|main)--perf-html\.netlify\.(?:com|app)\/?$/.test( + targetUrl + ) + ) { + // This URL is one of the allowed ones to be used for configuration. + return targetUrl; + } + + console.error( + `The preference "devtools.performance.recording.ui-base-url" was set to a ` + + "URL that is not allowed. No WebChannel messages will be sent between the " + + `browser and that URL. Falling back to ${frontEndUrl}. Only localhost ` + + "and deploy previews URLs are allowed.", + targetUrl + ); + } + + return frontEndUrl; +} + +XPCOMUtils.defineLazyGetter(this, "ProfilerPopupBackground", function() { + return ChromeUtils.import( + "resource://devtools/client/performance-new/popup/background.jsm.js" + ); +}); + +function DevToolsStartup() { + this.onEnabledPrefChanged = this.onEnabledPrefChanged.bind(this); + this.onWindowReady = this.onWindowReady.bind(this); + this.toggleProfilerKeyShortcuts = this.toggleProfilerKeyShortcuts.bind(this); +} + +DevToolsStartup.prototype = { + /** + * Boolean flag to check if DevTools have been already initialized or not. + * By initialized, we mean that its main modules are loaded. + */ + initialized: false, + + /** + * Boolean flag to check if the devtools initialization was already sent to telemetry. + * We only want to record one devtools entry point per Firefox run, but we are not + * interested in all the entry points. + */ + recorded: false, + + get telemetry() { + if (!this._telemetry) { + this._telemetry = new Telemetry(); + this._telemetry.setEventRecordingEnabled(true); + } + return this._telemetry; + }, + + /** + * Flag that indicates if the developer toggle was already added to customizableUI. + */ + developerToggleCreated: false, + + /** + * Flag that indicates if the profiler recording popup was already added to + * customizableUI. + */ + profilerRecordingButtonCreated: false, + + isDisabledByPolicy: function() { + return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false); + }, + + handle: function(cmdLine) { + const flags = this.readCommandLineFlags(cmdLine); + + // handle() can be called after browser startup (e.g. opening links from other apps). + const isInitialLaunch = + cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH; + if (isInitialLaunch) { + // Enable devtools for all users on startup (onboarding experiment from Bug 1408969 + // is over). + Services.prefs.setBoolPref(DEVTOOLS_ENABLED_PREF, true); + + // The F12 shortcut might be disabled to avoid accidental usage. + // Users who are already considered as devtools users should not be + // impacted. + if (this.isDevToolsUser()) { + Services.prefs.setBoolPref(DEVTOOLS_F12_DISABLED_PREF, false); + } + + // Store devtoolsFlag to check it later in onWindowReady. + this.devtoolsFlag = flags.devtools; + + /* eslint-disable mozilla/balanced-observers */ + // We are not expecting to remove those listeners until Firefox closes. + + // Only top level Firefox Windows fire a browser-delayed-startup-finished event + Services.obs.addObserver( + this.onWindowReady, + "browser-delayed-startup-finished" + ); + + // Update menu items when devtools.enabled changes. + Services.prefs.addObserver( + DEVTOOLS_ENABLED_PREF, + this.onEnabledPrefChanged + ); + /* eslint-enable mozilla/balanced-observers */ + + if (!this.isDisabledByPolicy()) { + if (AppConstants.MOZ_DEV_EDITION) { + // On DevEdition, the developer toggle is displayed by default in the navbar + // area and should be created before the first paint. + this.hookDeveloperToggle(); + } + + this.hookProfilerRecordingButton(); + } + } + + if (flags.console) { + this.commandLine = true; + this.handleConsoleFlag(cmdLine); + } + if (flags.debugger) { + this.commandLine = true; + const binaryPath = + typeof flags.debugger == "string" ? flags.debugger : null; + this.handleDebuggerFlag(cmdLine, binaryPath); + } + + if (flags.devToolsServer) { + this.handleDevToolsServerFlag(cmdLine, flags.devToolsServer); + } + }, + + readCommandLineFlags(cmdLine) { + // All command line flags are disabled if DevTools are disabled by policy. + if (this.isDisabledByPolicy()) { + return { + console: false, + debugger: false, + devtools: false, + devToolsServer: false, + }; + } + + const console = cmdLine.handleFlag("jsconsole", false); + const devtools = cmdLine.handleFlag("devtools", false); + + let devToolsServer; + try { + devToolsServer = cmdLine.handleFlagWithParam( + "start-debugger-server", + false + ); + } catch (e) { + // We get an error if the option is given but not followed by a value. + // By catching and trying again, the value is effectively optional. + devToolsServer = cmdLine.handleFlag("start-debugger-server", false); + } + + let debuggerFlag; + try { + debuggerFlag = cmdLine.handleFlagWithParam("jsdebugger", false); + } catch (e) { + // We get an error if the option is given but not followed by a value. + // By catching and trying again, the value is effectively optional. + debuggerFlag = cmdLine.handleFlag("jsdebugger", false); + } + + return { console, debugger: debuggerFlag, devtools, devToolsServer }; + }, + + /** + * Called when receiving the "browser-delayed-startup-finished" event for a new + * top-level window. + */ + onWindowReady(window) { + if (this.isDisabledByPolicy()) { + this.removeDevToolsMenus(window); + return; + } + + this.hookWindow(window); + + // This listener is called for all Firefox windows, but we want to execute some code + // only once. + if (!this._firstWindowReadyReceived) { + this.onFirstWindowReady(window); + this._firstWindowReadyReceived = true; + } + + JsonView.initialize(); + }, + + removeDevToolsMenus(window) { + // This will hide the "Tools > Web Developer" menu. + window.document + .getElementById("webDeveloperMenu") + .setAttribute("hidden", "true"); + // This will hide the "Web Developer" item in the hamburger menu. + PanelMultiView.getViewNode( + window.document, + "appMenu-developer-button" + ).setAttribute("hidden", "true"); + }, + + onFirstWindowReady(window) { + if (this.devtoolsFlag) { + this.handleDevToolsFlag(window); + + // In the case of the --jsconsole and --jsdebugger command line parameters + // there was no browser window when they were processed so we act on the + // this.commandline flag instead. + if (this.commandLine) { + this.sendEntryPointTelemetry("CommandLine"); + } + } + }, + + /** + * Register listeners to all possible entry points for Developer Tools. + * But instead of implementing the actual actions, defer to DevTools codebase. + * In most cases, it only needs to call this.initDevTools which handles the rest. + * We do that to prevent loading any DevTools module until the user intent to use them. + */ + hookWindow(window) { + // Key Shortcuts need to be added on all the created windows. + this.hookKeyShortcuts(window); + + // In some situations (e.g. starting Firefox with --jsconsole) DevTools will be + // initialized before the first browser-delayed-startup-finished event is received. + // We use a dedicated flag because we still need to hook the developer toggle. + this.hookDeveloperToggle(); + this.hookProfilerRecordingButton(); + + // The developer menu hook only needs to be added if devtools have not been + // initialized yet. + if (!this.initialized) { + this.hookWebDeveloperMenu(window); + } + + this.createDevToolsEnableMenuItem(window); + this.updateDevToolsMenuItems(window); + }, + + /** + * Dynamically register a wrench icon in the customization menu. + * You can use this button by right clicking on Firefox toolbar + * and dragging it from the customization panel to the toolbar. + * (i.e. this isn't displayed by default to users!) + * + * _But_, the "Web Developer" entry in the hamburger menu (the menu with + * 3 horizontal lines), is using this "developer-button" view to populate + * its menu. So we have to register this button for the menu to work. + * + * Also, this menu duplicates its own entries from the "Web Developer" + * menu in the system menu, under "Tools" main menu item. The system + * menu is being hooked by "hookWebDeveloperMenu" which ends up calling + * devtools/client/framework/browser-menus to create the items for real, + * initDevTools, from onViewShowing is also calling browser-menu. + */ + hookDeveloperToggle() { + if (this.developerToggleCreated) { + return; + } + + const id = "developer-button"; + const widget = CustomizableUI.getWidget(id); + if (widget && widget.provider == CustomizableUI.PROVIDER_API) { + return; + } + const item = { + id: id, + type: "view", + viewId: "PanelUI-developer", + shortcutId: "key_toggleToolbox", + tooltiptext: "developer-button.tooltiptext2", + onViewShowing: event => { + if (Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF)) { + // If DevTools are enabled, initialize DevTools to create all menuitems in the + // system menu before trying to copy them. + this.initDevTools("HamburgerMenu"); + } + + // Populate the subview with whatever menuitems are in the developer + // menu. We skip menu elements, because the menu panel has no way + // of dealing with those right now. + const doc = event.target.ownerDocument; + + const menu = doc.getElementById("menuWebDeveloperPopup"); + + const itemsToDisplay = [...menu.children]; + // Hardcode the addition of the "work offline" menuitem at the bottom: + itemsToDisplay.push({ + localName: "menuseparator", + getAttribute: () => {}, + }); + itemsToDisplay.push(doc.getElementById("goOfflineMenuitem")); + + const developerItems = PanelMultiView.getViewNode( + doc, + "PanelUI-developerItems" + ); + CustomizableUI.clearSubview(developerItems); + CustomizableUI.fillSubviewFromMenuItems(itemsToDisplay, developerItems); + }, + onInit(anchor) { + // Since onBeforeCreated already bails out when initialized, we can call + // it right away. + this.onBeforeCreated(anchor.ownerDocument); + }, + onBeforeCreated: doc => { + // The developer toggle needs the "key_toggleToolbox" <key> element. + // In DEV EDITION, the toggle is added before 1st paint and hookKeyShortcuts() is + // not called yet when CustomizableUI creates the widget. + this.hookKeyShortcuts(doc.defaultView); + + if (PanelMultiView.getViewNode(doc, "PanelUI-developerItems")) { + return; + } + const view = doc.createXULElement("panelview"); + view.id = "PanelUI-developerItems"; + const panel = doc.createXULElement("vbox"); + panel.setAttribute("class", "panel-subview-body"); + view.appendChild(panel); + doc.getElementById("PanelUI-multiView").appendChild(view); + }, + }; + CustomizableUI.createWidget(item); + CustomizableWidgets.push(item); + + this.developerToggleCreated = true; + }, + + /** + * Register the profiler recording button. This button will be available + * in the customization palette for the Firefox toolbar. In addition, it can be + * enabled from profiler.firefox.com. + */ + hookProfilerRecordingButton() { + if (this.profilerRecordingButtonCreated) { + return; + } + const featureFlagPref = "devtools.performance.popup.feature-flag"; + const isPopupFeatureFlagEnabled = Services.prefs.getBoolPref( + featureFlagPref + ); + this.profilerRecordingButtonCreated = true; + + // Listen for messages from the front-end. This needs to happen even if the + // button isn't enabled yet. This will allow the front-end to turn on the + // popup for our users, regardless of if the feature is enabled by default. + this.initializeProfilerWebChannel(); + + if (isPopupFeatureFlagEnabled) { + // Initialize the CustomizableUI widget. + ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts); + } else { + // The feature flag is not enabled, but watch for it to be enabled. If it is, + // initialize everything. + const enable = () => { + ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts); + Services.prefs.removeObserver(featureFlagPref, enable); + }; + Services.prefs.addObserver(featureFlagPref, enable); + } + }, + + /** + * Initialize the WebChannel for profiler.firefox.com. This function happens at + * startup, so care should be taken to minimize its performance impact. The WebChannel + * is a mechanism that is used to communicate between the browser, and front-end code. + */ + initializeProfilerWebChannel() { + let channel; + + // Register a channel for the URL in preferences. Also update the WebChannel if + // the URL changes. + const urlPref = "devtools.performance.recording.ui-base-url"; + + // This method is only run once per Firefox instance, so it should not be + // strictly necessary to remove observers here. + // eslint-disable-next-line mozilla/balanced-observers + Services.prefs.addObserver(urlPref, registerWebChannel); + + registerWebChannel(); + + function registerWebChannel() { + if (channel) { + channel.stopListening(); + } + + const urlForWebChannel = Services.io.newURI( + validateProfilerWebChannelUrl(Services.prefs.getStringPref(urlPref)) + ); + + channel = new WebChannel("profiler.firefox.com", urlForWebChannel); + + channel.listen((id, message, target) => { + // Defer loading the ProfilerPopupBackground script until it's absolutely needed, + // as this code path gets loaded at startup. + ProfilerPopupBackground.handleWebChannelMessage( + channel, + id, + message, + target + ); + }); + } + }, + + /* + * We listen to the "Web Developer" system menu, which is under "Tools" main item. + * This menu item is hardcoded empty in Firefox UI. We listen for its opening to + * populate it lazily. Loading main DevTools module is going to populate it. + */ + hookWebDeveloperMenu(window) { + const menu = window.document.getElementById("webDeveloperMenu"); + const onPopupShowing = () => { + if (!Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF)) { + return; + } + menu.removeEventListener("popupshowing", onPopupShowing); + this.initDevTools("SystemMenu"); + }; + menu.addEventListener("popupshowing", onPopupShowing); + }, + + /** + * Create a new menu item to enable DevTools and insert it DevTools's submenu in the + * System Menu. + */ + createDevToolsEnableMenuItem(window) { + const { document } = window; + + // Create the menu item. + const item = document.createXULElement("menuitem"); + item.id = "enableDeveloperTools"; + item.setAttribute( + "label", + StartupBundle.GetStringFromName("enableDevTools.label") + ); + item.setAttribute( + "accesskey", + StartupBundle.GetStringFromName("enableDevTools.accesskey") + ); + + // The menu item should open the install page for DevTools. + item.addEventListener("command", () => { + this.openInstallPage("SystemMenu"); + }); + + // Insert the menu item in the DevTools submenu. + const systemMenuItem = document.getElementById("menuWebDeveloperPopup"); + systemMenuItem.appendChild(item); + }, + + /** + * Update the visibility the menu item to enable DevTools. + */ + updateDevToolsMenuItems(window) { + const item = window.document.getElementById("enableDeveloperTools"); + item.hidden = Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF); + }, + + /** + * Loop on all windows and update the hidden attribute of the "enable DevTools" menu + * item. + */ + onEnabledPrefChanged() { + for (const window of Services.wm.getEnumerator("navigator:browser")) { + if (window.gBrowserInit && window.gBrowserInit.delayedStartupFinished) { + this.updateDevToolsMenuItems(window); + } + } + }, + + /** + * Check if the user is a DevTools user by looking at our selfxss pref. + * This preference is incremented everytime the console is used (up to 5). + * + * @return {Boolean} true if the user can be considered as a devtools user. + */ + isDevToolsUser() { + const selfXssCount = Services.prefs.getIntPref("devtools.selfxss.count", 0); + return selfXssCount > 0; + }, + + hookKeyShortcuts(window) { + const doc = window.document; + + // hookKeyShortcuts can be called both from hookWindow and from the developer toggle + // onBeforeCreated. Make sure shortcuts are only added once per window. + if (doc.getElementById("devtoolsKeyset")) { + return; + } + + const keyset = doc.createXULElement("keyset"); + keyset.setAttribute("id", "devtoolsKeyset"); + + this.attachKeys(doc, KeyShortcuts, keyset); + + // Appending a <key> element is not always enough. The <keyset> needs + // to be detached and reattached to make sure the <key> is taken into + // account (see bug 832984). + const mainKeyset = doc.getElementById("mainKeyset"); + mainKeyset.parentNode.insertBefore(keyset, mainKeyset); + }, + + /** + * This method attaches on the key elements to the devtools keyset. + */ + attachKeys(doc, keyShortcuts, keyset = doc.getElementById("devtoolsKeyset")) { + const window = doc.defaultView; + for (const key of keyShortcuts) { + if (!key.shortcut) { + // Shortcuts might be missing when a user relies on a language packs + // which is missing a recently uplifted shortcut. Language packs are + // typically updated a few days after a code uplift. + continue; + } + const xulKey = this.createKey(doc, key, () => this.onKey(window, key)); + keyset.appendChild(xulKey); + } + }, + + /** + * This method removes keys from the devtools keyset. + */ + removeKeys(doc, keyShortcuts) { + for (const key of keyShortcuts) { + const keyElement = doc.getElementById(this.getKeyElementId(key)); + if (keyElement) { + keyElement.remove(); + } + } + }, + + /** + * We only want to have the keyboard shortcuts active when the menu button is on. + * This function either adds or removes the elements. + * @param {boolean} isEnabled + */ + toggleProfilerKeyShortcuts(isEnabled) { + const profilerKeyShortcuts = getProfilerKeyShortcuts(); + for (const { document } of Services.wm.getEnumerator(null)) { + const devtoolsKeyset = document.getElementById("devtoolsKeyset"); + const mainKeyset = document.getElementById("mainKeyset"); + + if (!devtoolsKeyset || !mainKeyset) { + // There may not be devtools keyset on this window. + continue; + } + + const areProfilerKeysPresent = !!document.getElementById( + "key_profilerStartStop" + ); + if (isEnabled === areProfilerKeysPresent) { + // Don't double add or double remove the shortcuts. + continue; + } + if (isEnabled) { + this.attachKeys(document, profilerKeyShortcuts); + } else { + this.removeKeys(document, profilerKeyShortcuts); + } + // Appending a <key> element is not always enough. The <keyset> needs + // to be detached and reattached to make sure the <key> is taken into + // account (see bug 832984). + mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset); + } + }, + + async onKey(window, key) { + try { + // The profiler doesn't care if DevTools is loaded, so provide a quick check + // first to bail out of checking if DevTools is available. + switch (key.id) { + case "profilerStartStop": { + ProfilerPopupBackground.toggleProfiler("aboutprofiling"); + return; + } + case "profilerCapture": { + ProfilerPopupBackground.captureProfile("aboutprofiling"); + return; + } + } + if (!Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF)) { + const id = key.toolId || key.id; + this.openInstallPage("KeyShortcut", id); + } else { + // Record the timing at which this event started in order to compute later in + // gDevTools.showToolbox, the complete time it takes to open the toolbox. + // i.e. especially take `initDevTools` into account. + const startTime = Cu.now(); + const require = this.initDevTools("KeyShortcut", key); + const { + gDevToolsBrowser, + } = require("devtools/client/framework/devtools-browser"); + await gDevToolsBrowser.onKeyShortcut(window, key, startTime); + } + } catch (e) { + console.error(`Exception while trigerring key ${key}: ${e}\n${e.stack}`); + } + }, + + getKeyElementId({ id, toolId }) { + return "key_" + (id || toolId); + }, + + // Create a <xul:key> DOM Element + createKey(doc, key, oncommand) { + const { shortcut, modifiers: mod } = key; + const k = doc.createXULElement("key"); + k.id = this.getKeyElementId(key); + + if (shortcut.startsWith("VK_")) { + k.setAttribute("keycode", shortcut); + if (shortcut.match(/^VK_\d$/)) { + // Add the event keydown attribute to ensure that shortcuts work for combinations + // such as ctrl shift 1. + k.setAttribute("event", "keydown"); + } + } else { + k.setAttribute("key", shortcut); + } + + if (mod) { + k.setAttribute("modifiers", mod); + } + + // Bug 371900: command event is fired only if "oncommand" attribute is set. + k.setAttribute("oncommand", ";"); + k.addEventListener("command", oncommand); + + return k; + }, + + initDevTools: function(reason, key = "") { + // If an entry point is fired and tools are not enabled open the installation page + if (!Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF)) { + this.openInstallPage(reason); + return null; + } + + // In the case of the --jsconsole and --jsdebugger command line parameters + // there is no browser window yet so we don't send any telemetry yet. + if (reason !== "CommandLine") { + this.sendEntryPointTelemetry(reason, key); + } + + this.initialized = true; + const { require } = ChromeUtils.import( + "resource://devtools/shared/Loader.jsm" + ); + // Ensure loading main devtools module that hooks up into browser UI + // and initialize all devtools machinery. + require("devtools/client/framework/devtools-browser"); + return require; + }, + + /** + * Open about:devtools to start the onboarding flow. + * + * @param {String} reason + * One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu", + * "CommandLine". + * @param {String} keyId + * Optional. If the onboarding flow was triggered by a keyboard shortcut, pass + * the shortcut key id (or toolId) to about:devtools. + */ + openInstallPage: function(reason, keyId) { + // If DevTools are completely disabled, bail out here as this might be called directly + // from other files. + if (this.isDisabledByPolicy()) { + return; + } + + const { gBrowser } = Services.wm.getMostRecentWindow("navigator:browser"); + + // Focus about:devtools tab if there is already one opened in the current window. + for (const tab of gBrowser.tabs) { + const browser = tab.linkedBrowser; + // browser.documentURI might be undefined if the browser tab is still loading. + const location = browser.documentURI ? browser.documentURI.spec : ""; + if ( + location.startsWith("about:devtools") && + !location.startsWith("about:devtools-toolbox") + ) { + // Focus the existing about:devtools tab and bail out. + gBrowser.selectedTab = tab; + return; + } + } + + let url = "about:devtools"; + + const params = []; + if (reason) { + params.push("reason=" + encodeURIComponent(reason)); + } + + const selectedBrowser = gBrowser.selectedBrowser; + if (selectedBrowser) { + params.push("tabid=" + selectedBrowser.outerWindowID); + } + + if (keyId) { + params.push("keyid=" + keyId); + } + + if (params.length > 0) { + url += "?" + params.join("&"); + } + + // Set relatedToCurrent: true to open the tab next to the current one. + gBrowser.selectedTab = gBrowser.addTrustedTab(url, { + relatedToCurrent: true, + }); + }, + + handleConsoleFlag: function(cmdLine) { + const window = Services.wm.getMostRecentWindow("devtools:webconsole"); + if (!window) { + const require = this.initDevTools("CommandLine"); + const { + BrowserConsoleManager, + } = require("devtools/client/webconsole/browser-console-manager"); + BrowserConsoleManager.toggleBrowserConsole().catch(console.error); + } else { + // the Browser Console was already open + window.focus(); + } + + if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { + cmdLine.preventDefault = true; + } + }, + + // Open the toolbox on the selected tab once the browser starts up. + handleDevToolsFlag: async function(window) { + const require = this.initDevTools("CommandLine"); + const { gDevTools } = require("devtools/client/framework/devtools"); + const { TargetFactory } = require("devtools/client/framework/target"); + const target = await TargetFactory.forTab(window.gBrowser.selectedTab); + gDevTools.showToolbox(target); + }, + + _isRemoteDebuggingEnabled() { + let remoteDebuggingEnabled = false; + try { + remoteDebuggingEnabled = kDebuggerPrefs.every(pref => { + return Services.prefs.getBoolPref(pref); + }); + } catch (ex) { + console.error(ex); + return false; + } + if (!remoteDebuggingEnabled) { + const errorMsg = + "Could not run chrome debugger! You need the following " + + "prefs to be set to true: " + + kDebuggerPrefs.join(", "); + console.error(new Error(errorMsg)); + // Dump as well, as we're doing this from a commandline, make sure people + // don't miss it: + dump(errorMsg + "\n"); + } + return remoteDebuggingEnabled; + }, + + handleDebuggerFlag: function(cmdLine, binaryPath) { + if (!this._isRemoteDebuggingEnabled()) { + return; + } + + let devtoolsThreadResumed = false; + const pauseOnStartup = cmdLine.handleFlag("wait-for-jsdebugger", false); + if (pauseOnStartup) { + const observe = function(subject, topic, data) { + devtoolsThreadResumed = true; + Services.obs.removeObserver(observe, "devtools-thread-ready"); + }; + Services.obs.addObserver(observe, "devtools-thread-ready"); + } + + const { BrowserToolboxLauncher } = ChromeUtils.import( + "resource://devtools/client/framework/browser-toolbox/Launcher.jsm" + ); + BrowserToolboxLauncher.init(null, null, null, binaryPath); + + if (pauseOnStartup) { + // Spin the event loop until the debugger connects. + const tm = Cc["@mozilla.org/thread-manager;1"].getService(); + tm.spinEventLoopUntil(() => { + return devtoolsThreadResumed; + }); + } + + if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { + cmdLine.preventDefault = true; + } + }, + + /** + * Handle the --start-debugger-server command line flag. The options are: + * --start-debugger-server + * The portOrPath parameter is boolean true in this case. Reads and uses the defaults + * from devtools.debugger.remote-port and devtools.debugger.remote-websocket prefs. + * The default values of these prefs are port 6000, WebSocket disabled. + * + * --start-debugger-server 6789 + * Start the non-WebSocket server on port 6789. + * + * --start-debugger-server /path/to/filename + * Start the server on a Unix domain socket. + * + * --start-debugger-server ws:6789 + * Start the WebSocket server on port 6789. + * + * --start-debugger-server ws: + * Start the WebSocket server on the default port (taken from d.d.remote-port) + */ + handleDevToolsServerFlag: function(cmdLine, portOrPath) { + if (!this._isRemoteDebuggingEnabled()) { + return; + } + + let webSocket = false; + const defaultPort = Services.prefs.getIntPref( + "devtools.debugger.remote-port" + ); + if (portOrPath === true) { + // Default to pref values if no values given on command line + webSocket = Services.prefs.getBoolPref( + "devtools.debugger.remote-websocket" + ); + portOrPath = defaultPort; + } else if (portOrPath.startsWith("ws:")) { + webSocket = true; + const port = portOrPath.slice(3); + portOrPath = Number(port) ? port : defaultPort; + } + + const { DevToolsLoader } = ChromeUtils.import( + "resource://devtools/shared/Loader.jsm" + ); + + try { + // Create a separate loader instance, so that we can be sure to receive + // a separate instance of the DebuggingServer from the rest of the + // devtools. This allows us to safely use the tools against even the + // actors and DebuggingServer itself, especially since we can mark + // serverLoader as invisible to the debugger (unlike the usual loader + // settings). + const serverLoader = new DevToolsLoader({ + invisibleToDebugger: true, + }); + const { DevToolsServer: devToolsServer } = serverLoader.require( + "devtools/server/devtools-server" + ); + const { SocketListener } = serverLoader.require( + "devtools/shared/security/socket" + ); + devToolsServer.init(); + devToolsServer.registerAllActors(); + devToolsServer.allowChromeProcess = true; + const socketOptions = { portOrPath, webSocket }; + + const listener = new SocketListener(devToolsServer, socketOptions); + listener.open(); + dump("Started devtools server on " + portOrPath + "\n"); + } catch (e) { + dump("Unable to start devtools server on " + portOrPath + ": " + e); + } + + if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { + cmdLine.preventDefault = true; + } + }, + + /** + * Send entry point telemetry explaining how the devtools were launched. This + * functionality also lives inside `devtools/client/framework/browser-menus.js` + * because this codepath is only used the first time a toolbox is opened for a + * tab. + * + * @param {String} reason + * One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu", + * "CommandLine". + * @param {String} key + * The key used by a key shortcut. + */ + sendEntryPointTelemetry(reason, key = "") { + if (!reason) { + return; + } + + let keys = ""; + + if (reason === "KeyShortcut") { + let { modifiers, shortcut } = key; + + modifiers = modifiers.replace(",", "+"); + + if (shortcut.startsWith("VK_")) { + shortcut = shortcut.substr(3); + } + + keys = `${modifiers}+${shortcut}`; + } + + const window = Services.wm.getMostRecentWindow("navigator:browser"); + + this.telemetry.addEventProperty( + window, + "open", + "tools", + null, + "shortcut", + keys + ); + this.telemetry.addEventProperty( + window, + "open", + "tools", + null, + "entrypoint", + reason + ); + + if (this.recorded) { + return; + } + + // Only save the first call for each firefox run as next call + // won't necessarely start the tool. For example key shortcuts may + // only change the currently selected tool. + try { + this.telemetry.getHistogramById("DEVTOOLS_ENTRY_POINT").add(reason); + } catch (e) { + dump("DevTools telemetry entry point failed: " + e + "\n"); + } + this.recorded = true; + }, + + // Used by tests and the toolbox to register the same key shortcuts in toolboxes loaded + // in a window window. + get KeyShortcuts() { + return KeyShortcuts; + }, + get wrappedJSObject() { + return this; + }, + + /* eslint-disable max-len */ + helpInfo: + " --jsconsole Open the Browser Console.\n" + + " --jsdebugger [<path>] Open the Browser Toolbox. Defaults to the local build\n" + + " but can be overridden by a firefox path.\n" + + " --wait-for-jsdebugger Spin event loop until JS debugger connects.\n" + + " Enables debugging (some) application startup code paths.\n" + + " Only has an effect when `--jsdebugger` is also supplied.\n" + + " --devtools Open DevTools on initial load.\n" + + " --start-debugger-server [ws:][ <port> | <path> ] Start the devtools server on\n" + + " a TCP port or Unix domain socket path. Defaults to TCP port\n" + + " 6000. Use WebSocket protocol if ws: prefix is specified.\n", + /* eslint-disable max-len */ + + classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"), + QueryInterface: ChromeUtils.generateQI(["nsICommandLineHandler"]), +}; + +/** + * Singleton object that represents the JSON View in-content tool. + * It has the same lifetime as the browser. + */ +const JsonView = { + initialized: false, + + initialize: function() { + // Prevent loading the frame script multiple times if we call this more than once. + if (this.initialized) { + return; + } + this.initialized = true; + + // Register for messages coming from the child process. + // This is never removed as there is no particular need to unregister + // it during shutdown. + Services.mm.addMessageListener("devtools:jsonview:save", this.onSave); + }, + + // Message handlers for events from child processes + + /** + * Save JSON to a file needs to be implemented here + * in the parent process. + */ + onSave: function(message) { + const browser = message.target; + const chrome = browser.ownerGlobal; + if (message.data === null) { + // Save original contents + chrome.saveBrowser(browser); + } else { + if ( + !message.data.startsWith("blob:null") || + !browser.contentPrincipal.isNullPrincipal + ) { + Cu.reportError("Got invalid request to save JSON data"); + return; + } + // The following code emulates saveBrowser, but: + // - Uses the given blob URL containing the custom contents to save. + // - Obtains the file name from the URL of the document, not the blob. + // - avoids passing the document and explicitly passes system principal. + // We have a blob created by a null principal to save, and the null + // principal is from the child. Null principals don't survive crossing + // over IPC, so there's no other principal that'll work. + const persistable = browser.frameLoader; + persistable.startPersistence(null, { + onDocumentReady(doc) { + const uri = chrome.makeURI(doc.documentURI, doc.characterSet); + const filename = chrome.getDefaultFileName(undefined, uri, doc, null); + chrome.internalSave( + message.data, + null, + filename, + null, + doc.contentType, + false /* bypass cache */, + null /* filepicker title key */, + null /* file chosen */, + null /* referrer */, + doc.cookieJarSettings, + null /* initiating document */, + false /* don't skip prompt for a location */, + null /* cache key */, + PrivateBrowsingUtils.isBrowserPrivate( + browser + ) /* private browsing ? */, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }, + onError(status) { + throw new Error("JSON Viewer's onSave failed in startPersistence"); + }, + }); + } + }, +}; + +var EXPORTED_SYMBOLS = ["DevToolsStartup", "validateProfilerWebChannelUrl"]; |