Bug 1466534 - Centralize DevTools link handling. r=jdescottes draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Mon, 11 Jun 2018 23:42:19 -0700
changeset 808472 f11fb3788ded4442df4b315c0e426e6b5687fd83
parent 808416 257c191e7903523a1132e04460a0b2460d950809
push id113402
push userbmo:jryans@gmail.com
push dateTue, 19 Jun 2018 19:10:18 +0000
reviewersjdescottes
bugs1466534
milestone62.0a1
Bug 1466534 - Centralize DevTools link handling. r=jdescottes This adds a `openContentLink` helper meant for any link controlled by web content. If there is an associated toolbox open, the toolbox's tab is used to supply a triggering principal for the link. MozReview-Commit-ID: 45l5yAPGpZr
devtools/client/accessibility/components/Accessible.js
devtools/client/accessibility/test/mochitest/test_accessible_openLink.html
devtools/client/application/src/components/WorkerListEmpty.js
devtools/client/debugger/content/views/sources-view.js
devtools/client/debugger/new/panel.js
devtools/client/dom/dom-panel.js
devtools/client/framework/components/ToolboxToolbar.js
devtools/client/framework/target.js
devtools/client/inspector/computed/computed.js
devtools/client/inspector/inspector.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/inspector/shared/three-pane-onboarding-tooltip.js
devtools/client/menus.js
devtools/client/netmonitor/src/utils/open-request-in-tab.js
devtools/client/performance-new/components/Description.js
devtools/client/scratchpad/scratchpad.js
devtools/client/shared/components/MdnLink.js
devtools/client/shared/link.js
devtools/client/shared/test/browser_link.js
devtools/client/styleeditor/StyleEditorUI.jsm
devtools/client/styleeditor/test/browser_styleeditor_opentab.js
devtools/client/webconsole/hudservice.js
devtools/client/webconsole/test/mochitest/browser_webconsole_allow_mixedcontent_securityerrors.js
devtools/client/webconsole/test/mochitest/browser_webconsole_network_messages_status_code.js
devtools/client/webconsole/test/mochitest/head.js
devtools/client/webconsole/utils/context-menu.js
devtools/client/webide/content/webide.js
devtools/shared/gcli/commands/screenshot.js
--- a/devtools/client/accessibility/components/Accessible.js
+++ b/devtools/client/accessibility/components/Accessible.js
@@ -19,16 +19,18 @@ const {flashElementOn, flashElementOff} 
       require("devtools/client/inspector/markup/utils");
 const { updateDetails } = require("../actions/details");
 
 const Tree = createFactory(require("devtools/client/shared/components/VirtualizedTree"));
 // Reps
 const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
 const { Rep, ElementNode } = REPS;
 
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
+
 const TELEMETRY_NODE_INSPECTED_COUNT = "devtools.accessibility.node_inspected_count";
 
 class AccessiblePropertyClass extends Component {
   static get propTypes() {
     return {
       accessible: PropTypes.string,
       object: PropTypes.any,
       focused: PropTypes.bool,
@@ -170,33 +172,17 @@ class Accessible extends Component {
       return;
     }
 
     gToolbox.selectTool("inspector").then(() =>
       gToolbox.selection.setNodeFront(nodeFront, reason));
   }
 
   openLink(link, e) {
-    if (!gToolbox) {
-      return;
-    }
-
-    // Avoid using Services.appinfo.OS in order to keep accessible pane's frontend free of
-    // priveleged code.
-    const os = window.navigator.userAgent;
-    const isOSX =  os && os.includes("Mac");
-    let where = "tab";
-    if (e && (e.button === 1 || (e.button === 0 && (isOSX ? e.metaKey : e.ctrlKey)))) {
-      where = "tabshifted";
-    } else if (e && e.shiftKey) {
-      where = "window";
-    }
-
-    const win = gToolbox.doc.defaultView.top;
-    win.openWebLinkIn(link, where);
+    openContentLink(link);
   }
 
   renderItem(item, depth, focused, arrow, expanded) {
     const object = item.contents;
     const valueProps = {
       object,
       mode: MODE.TINY,
       title: "Object",
--- a/devtools/client/accessibility/test/mochitest/test_accessible_openLink.html
+++ b/devtools/client/accessibility/test/mochitest/test_accessible_openLink.html
@@ -17,73 +17,61 @@ Test that openLink function is called if
 <pre id="test">
 <script src="head.js" type="application/javascript"></script>
 <script type="application/javascript">
 
 "use strict";
 
 window.onload = async function() {
   try {
+    const { gDevTools } = require("devtools/client/framework/devtools");
     const Services = browserRequire("Services");
     const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
     const { createFactory, createElement } =
       browserRequire("devtools/client/shared/vendor/react");
     const { Provider } = require("devtools/client/shared/vendor/react-redux");
     const createStore = require("devtools/client/shared/redux/create-store")();
     const { Simulate } =
       browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
     const Accessible = createFactory(
       browserRequire("devtools/client/accessibility/components/Accessible"));
 
     function testLinkClicked(link, event, expectedUrl, expectedWhere) {
+      const browserWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+      const defaultOpenWebLinkIn = browserWindow.openWebLinkIn;
+
       const checker = Symbol();
       let onClickArgs = checker;
-      const mockToolbox = {
-        doc: {
-          defaultView: {
-            top: {
-              openWebLinkIn: (url, where) => {
-                onClickArgs = { url, where };
-              }
-            }
-          }
-        }
+      browserWindow.openWebLinkIn = (url, where) => {
+        onClickArgs = { url, where };
       };
 
-      window.gToolbox = mockToolbox;
       Simulate.click(link, event);
 
       ok(onClickArgs !== checker, "Link was clicked");
       is(onClickArgs.url, expectedUrl, "Correct URL is opened");
       is(onClickArgs.where, expectedWhere, "URL was opened correctly");
 
-      window.gToolbox = null;
+      browserWindow.openWebLinkIn = defaultOpenWebLinkIn;
     }
 
     const a = Accessible({ labelledby: "Test Accessible" });
     ok(a, "Should be able to create Accessible instances");
 
     let URL = "http://example.com";
     const mockStore = createStore((state, action) =>
       action ? { ...state, ...action } : state,
       { details: { DOMNode: {}, accessible: { value: URL } } });
     const provider = createElement(Provider, { store: mockStore }, a);
     const accessible = ReactDOM.render(provider, window.document.body);
     ok(accessible, "Should be able to mount Accessible instances");
 
     let link = document.querySelector(".url");
     testLinkClicked(link, null, URL, "tab");
 
-    let event = { button: 0 };
-    event[Services.appinfo.OS == "Darwin" ? "metaKey" : "ctrlKey"] = true;
-    testLinkClicked(link, event, URL, "tabshifted");
-
-    event = { shiftKey: true };
-    testLinkClicked(link, event, URL, "window");
-
     URL = "non-URL";
     await mockStore.dispatch(
       { type: "update", details: { DOMNode: {}, accessible: { value: URL } } });
     link = document.querySelector(".url");
     ok(!link, "Non URL link should not be rendered as a link.");
   } catch (e) {
     ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
   } finally {
--- a/devtools/client/application/src/components/WorkerListEmpty.js
+++ b/devtools/client/application/src/components/WorkerListEmpty.js
@@ -1,15 +1,15 @@
 /* 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 { openWebLink, openTrustedLink } = require("devtools/client/shared/link");
+const { openDocLink, openTrustedLink } = require("devtools/client/shared/link");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { createFactory, Component } = require("devtools/client/shared/vendor/react");
 const { a, article, h1, li, p, ul } = require("devtools/client/shared/vendor/react-dom-factories");
 
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
 
 const DOC_URL = "https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers" +
@@ -34,17 +34,17 @@ class WorkerListEmpty extends Component 
     this.props.serviceContainer.selectTool("jsdebugger");
   }
 
   openAboutDebugging() {
     openTrustedLink("about:debugging#workers");
   }
 
   openDocumentation() {
-    openWebLink(DOC_URL);
+    openDocLink(DOC_URL);
   }
 
   render() {
     return article(
       { className: "worker-list-empty" },
       Localized({
         id: "serviceworker-empty-intro",
         a: a({ className: "external-link", onClick: () => this.openDocumentation() })
--- a/devtools/client/debugger/content/views/sources-view.js
+++ b/devtools/client/debugger/content/views/sources-view.js
@@ -23,17 +23,18 @@ const { bindActionCreators } = require("
 const { extend } = require("devtools/shared/extend");
 const {
   WidgetMethods,
   setNamedTimeout
 } = require("devtools/client/shared/widgets/view-helpers");
 const { Task } = require("devtools/shared/task");
 const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 const { gDevTools } = require("devtools/client/framework/devtools");
-const {KeyCodes} = require("devtools/client/shared/keycodes");
+const { KeyCodes } = require("devtools/client/shared/keycodes");
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
 const FUNCTION_SEARCH_POPUP_POSITION = "topcenter bottomleft";
 const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars
 const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start";
 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X = 7; // px
 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y = -3; // px
 
@@ -893,20 +894,18 @@ SourcesView.prototype = extend(WidgetMet
     }
     clipboardHelper.copyString(selected.source.url);
   },
 
   /**
    * Opens selected item source in a new tab.
    */
   _onNewTabCommand: function () {
-    let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
     let selected = this.selectedItem.attachment;
-    win.openWebLinkIn(selected.source.url, "tab", {
-      triggeringPrincipal: win.document.nodePrincipal,
+    openContentLink(selected.source.url, {
       relatedToCurrent: true,
     });
   },
 
   /**
    * Function called each time a breakpoint item is removed.
    *
    * @param object aItem
--- a/devtools/client/debugger/new/panel.js
+++ b/devtools/client/debugger/new/panel.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Task } = require("devtools/shared/task");
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const { gDevTools } = require("devtools/client/framework/devtools");
 const { TargetFactory } = require("devtools/client/framework/target");
 const { Toolbox } = require("devtools/client/framework/toolbox");
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
 const L10N = new LocalizationHelper(DBG_STRINGS_URI);
 
 function DebuggerPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this.panelWin.L10N = L10N;
   this.toolbox = toolbox;
@@ -58,34 +59,17 @@ DebuggerPanel.prototype = {
     };
   },
 
   _getState: function() {
     return this._store.getState();
   },
 
   openLink: function(url) {
-    const parentDoc = this.toolbox.doc;
-    if (!parentDoc) {
-      return;
-    }
-
-    const win = parentDoc.querySelector("window");
-    if (!win) {
-      return;
-    }
-
-    const top = win.ownerDocument.defaultView.top;
-    if (!top || typeof top.openWebLink !== "function") {
-      return;
-    }
-
-    top.openWebLinkIn(url, "tab", {
-      triggeringPrincipal: win.document.nodePrincipal
-    });
+    openContentLink(url);
   },
 
   openWorkerToolbox: async function(worker) {
     const [response, workerClient] =
       await this.toolbox.target.client.attachWorker(worker.actor);
     const workerTarget = TargetFactory.forWorker(workerClient);
     const toolbox = await gDevTools.showToolbox(workerTarget, "jsdebugger", Toolbox.HostType.WINDOW);
     toolbox.once("destroy", () => workerClient.detach());
--- a/devtools/client/dom/dom-panel.js
+++ b/devtools/client/dom/dom-panel.js
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Cu } = require("chrome");
 const ObjectClient = require("devtools/shared/client/object-client");
 
 const defer = require("devtools/shared/defer");
 const EventEmitter = require("devtools/shared/event-emitter");
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 /**
  * This object represents DOM panel. It's responsibility is to
  * render Document Object Model of the current debugger target.
  */
 function DomPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this._toolbox = toolbox;
@@ -174,20 +175,17 @@ DomPanel.prototype = {
     });
 
     this.pendingRequests.set(grip.actor, deferred.promise);
 
     return deferred.promise;
   },
 
   openLink: function(url) {
-    const parentDoc = this._toolbox.doc;
-    const iframe = parentDoc.getElementById("this._toolbox");
-    const top = iframe.ownerDocument.defaultView.top;
-    top.openWebLinkIn(url, "tab");
+    openContentLink(url);
   },
 
   getRootGrip: function() {
     const deferred = defer();
 
     // Attach Console. It might involve RDP communication, so wait
     // asynchronously for the result
     this.target.activeConsole.evaluateJSAsync("window", res => {
--- a/devtools/client/framework/components/ToolboxToolbar.js
+++ b/devtools/client/framework/components/ToolboxToolbar.js
@@ -2,17 +2,17 @@
  * 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 { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const {div, button} = dom;
-const {openWebLink} = require("devtools/client/shared/link");
+const {openDocLink} = require("devtools/client/shared/link");
 
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 const ToolboxTabs = createFactory(require("devtools/client/framework/components/ToolboxTabs"));
 
 /**
  * This is the overall component for the toolbox toolbar. It is designed to not know how
  * the state is being managed, and attempts to be as pure as possible. The
@@ -427,27 +427,27 @@ function showMeatballMenu(
     menu.append(new MenuItem({ type: "separator" }));
   }
 
   // Getting started
   menu.append(new MenuItem({
     id: "toolbox-meatball-menu-documentation",
     label: L10N.getStr("toolbox.meatballMenu.documentation.label"),
     click: () => {
-      openWebLink(
+      openDocLink(
         "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu");
     },
   }));
 
   // Give feedback
   menu.append(new MenuItem({
     id: "toolbox-meatball-menu-community",
     label: L10N.getStr("toolbox.meatballMenu.community.label"),
     click: () => {
-      openWebLink(
+      openDocLink(
         "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu");
     },
   }));
 
   const rect = menuButton.getBoundingClientRect();
   const screenX = menuButton.ownerDocument.defaultView.mozInnerScreenX;
   const screenY = menuButton.ownerDocument.defaultView.mozInnerScreenY;
 
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -385,16 +385,29 @@ TabTarget.prototype = {
       return parsedURL.pathname;
     } catch (e) {
       // Return the url if unable to resolve the pathname.
       return url;
     }
   },
 
   /**
+   * For local tabs, returns the tab's contentPrincipal, which can be used as a
+   * `triggeringPrincipal` when opening links.  However, this is a hack as it is not
+   * correct for subdocuments and it won't work for remote debugging.  Bug 1467945 hopes
+   * to devise a better approach.
+   */
+  get contentPrincipal() {
+    if (!this.isLocalTab) {
+      return null;
+    }
+    return this.tab.linkedBrowser.contentPrincipal;
+  },
+
+  /**
    * Adds remote protocol capabilities to the target, so that it can be used
    * for tools that support the Remote Debugging Protocol even for local
    * connections.
    */
   makeRemote: async function() {
     if (this._remote) {
       return this._remote.promise;
     }
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -22,16 +22,17 @@ const {
   VIEW_NODE_IMAGE_URL_TYPE,
   VIEW_NODE_FONT_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
 
 loader.lazyRequireGetter(this, "StyleInspectorMenu", "devtools/client/inspector/shared/style-inspector-menu");
 loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts");
 loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
 
 const FILTER_CHANGED_TIMEOUT = 150;
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
@@ -693,18 +694,17 @@ CssComputedView.prototype = {
   },
 
   _onClick: function(event) {
     const target = event.target;
 
     if (target.nodeName === "a") {
       event.stopPropagation();
       event.preventDefault();
-      const browserWin = this.inspector.target.tab.ownerDocument.defaultView;
-      browserWin.openWebLinkIn(target.href, "tab");
+      openContentLink(target.href);
     }
   },
 
   /**
    * Callback for copy event. Copy selected text.
    *
    * @param {Event} event
    *        copy event object.
@@ -1185,22 +1185,17 @@ PropertyView.prototype = {
     this.refreshMatchedSelectors();
     event.preventDefault();
   },
 
   /**
    * The action when a user clicks on the MDN help link for a property.
    */
   mdnLinkClick: function(event) {
-    const inspector = this.tree.inspector;
-
-    if (inspector.target.tab) {
-      const browserWin = inspector.target.tab.ownerDocument.defaultView;
-      browserWin.openWebLinkIn(this.link, "tab");
-    }
+    openContentLink(this.link);
   },
 
   /**
    * Destroy this property view, removing event listeners
    */
   destroy: function() {
     if (this._matchedSelectorViews) {
       for (const view of this._matchedSelectorViews) {
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -30,16 +30,17 @@ loader.lazyRequireGetter(this, "ToolSide
 loader.lazyRequireGetter(this, "MarkupView", "devtools/client/inspector/markup/markup");
 loader.lazyRequireGetter(this, "HighlightersOverlay", "devtools/client/inspector/shared/highlighters-overlay");
 loader.lazyRequireGetter(this, "nodeConstants", "devtools/shared/dom-node-constants");
 loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
 loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
 loader.lazyRequireGetter(this, "ExtensionSidebar", "devtools/client/inspector/extensions/extension-sidebar");
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
 loader.lazyGetter(this, "TOOLBOX_L10N", function() {
   return new LocalizationHelper("devtools/client/locales/toolbox.properties");
 });
 
@@ -2353,18 +2354,17 @@ Inspector.prototype = {
       return;
     }
 
     if (type === "uri" || type === "cssresource" || type === "jsresource") {
       // Open link in a new tab.
       this.inspector.resolveRelativeURL(
         link, this.selection.nodeFront).then(url => {
           if (type === "uri") {
-            const browserWin = this.target.tab.ownerDocument.defaultView;
-            browserWin.openWebLinkIn(url, "tab");
+            openContentLink(url);
           } else if (type === "cssresource") {
             return this.toolbox.viewSourceInStyleEditor(url);
           } else if (type === "jsresource") {
             return this.toolbox.viewSourceInDebugger(url);
           }
           return null;
         }).catch(console.error);
     } else if (type == "idref") {
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -1,29 +1,32 @@
 /* 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 {l10n} = require("devtools/shared/inspector/css-logic");
+const Services = require("Services");
+
+const { l10n } = require("devtools/shared/inspector/css-logic");
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 const {InplaceEditor, editableField} =
       require("devtools/client/shared/inplace-editor");
 const {
   createChild,
   appendText,
   advanceValidate,
   blurOnMultipleProperties
 } = require("devtools/client/inspector/shared/utils");
 const {
   parseDeclarations,
   parseSingleValue,
 } = require("devtools/shared/css/parsing-utils");
-const Services = require("Services");
+
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const SHARED_SWATCH_CLASS = "ruleview-swatch";
 const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
 const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
 const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
 const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
@@ -279,18 +282,17 @@ TextPropertyEditor.prototype = {
       });
 
       this.valueSpan.addEventListener("click", (event) => {
         const target = event.target;
 
         if (target.nodeName === "a") {
           event.stopPropagation();
           event.preventDefault();
-          const browserWin = this.ruleView.inspector.target.tab.ownerDocument.defaultView;
-          browserWin.openWebLinkIn(target.href, "tab");
+          openContentLink(target.href);
         }
       });
 
       editableField({
         start: this._onStartEditing,
         element: this.valueSpan,
         done: this._onValueDone,
         destroy: this.update,
--- a/devtools/client/inspector/shared/three-pane-onboarding-tooltip.js
+++ b/devtools/client/inspector/shared/three-pane-onboarding-tooltip.js
@@ -1,16 +1,16 @@
 /* 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 { openWebLink } = require("devtools/client/shared/link");
+const { openDocLink } = require("devtools/client/shared/link");
 const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 const SHOW_THREE_PANE_ONBOARDING_PREF = "devtools.inspector.show-three-pane-tooltip";
 
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
@@ -98,13 +98,13 @@ class ThreePaneOnboardingTooltip {
 
   /**
    * Handler for the "click" event on the learn more button. Hides the onboarding tooltip
    * and opens the link to the mdn page in a new tab.
    */
   onLearnMoreLinkClick() {
     Services.prefs.setBoolPref(SHOW_THREE_PANE_ONBOARDING_PREF, false);
     this.tooltip.hide();
-    openWebLink(LEARN_MORE_LINK);
+    openDocLink(LEARN_MORE_LINK);
   }
 }
 
 module.exports = ThreePaneOnboardingTooltip;
--- a/devtools/client/menus.js
+++ b/devtools/client/menus.js
@@ -29,16 +29,17 @@
  */
 
 const { Cu } = require("chrome");
 
 loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
 loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true);
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
 loader.lazyRequireGetter(this, "ResponsiveUIManager", "devtools/client/responsive.html/manager", true);
+loader.lazyRequireGetter(this, "openDocLink", "devtools/client/shared/link", true);
 
 loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm");
 loader.lazyImporter(this, "ScratchpadManager", "resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 
 exports.menuitems = [
   { id: "menu_devToolbox",
     l10nKey: "devToolboxMenuItem",
     oncommand(event) {
@@ -127,13 +128,12 @@ exports.menuitems = [
     }
   },
   { separator: true,
     id: "devToolsEndSeparator"
   },
   { id: "getMoreDevtools",
     l10nKey: "getMoreDevtoolsCmd",
     oncommand(event) {
-      const window = event.target.ownerDocument.defaultView;
-      window.openWebLinkIn("https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/", "tab");
+      openDocLink("https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/");
     }
   },
 ];
--- a/devtools/client/netmonitor/src/utils/open-request-in-tab.js
+++ b/devtools/client/netmonitor/src/utils/open-request-in-tab.js
@@ -9,26 +9,24 @@
 // implement the similar functionalities on its own.
 //
 // Please keep in mind that if the feature in this file has changed, don't
 // forget to also change that accordingly in
 // devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js.
 
 "use strict";
 
-const Services = require("Services");
-const { gDevTools } = require("devtools/client/framework/devtools");
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 /**
  * Opens given request in a new tab.
  */
 function openRequestInTab(url, requestPostData) {
-  const win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
   if (!requestPostData) {
-    win.openWebLinkIn(url, "tab", {relatedToCurrent: true});
+    openContentLink(url, {relatedToCurrent: true});
   } else {
     openPostRequestInTabHelper({
       url,
       data: requestPostData.postData
     });
   }
 }
 
--- a/devtools/client/performance-new/components/Description.js
+++ b/devtools/client/performance-new/components/Description.js
@@ -1,16 +1,16 @@
 /* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
 const { div, button, p } = require("devtools/client/shared/vendor/react-dom-factories");
-const { openWebLink } = require("devtools/client/shared/link");
+const { openDocLink } = require("devtools/client/shared/link");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const selectors = require("devtools/client/performance-new/store/selectors");
 
 /**
  * This component provides a helpful description for what is going on in the component
  * and provides some external links.
  */
@@ -23,17 +23,17 @@ class Description extends PureComponent 
   }
 
   constructor(props) {
     super(props);
     this.handleLinkClick = this.handleLinkClick.bind(this);
   }
 
   handleLinkClick(event) {
-    openWebLink(event.target.value);
+    openDocLink(event.target.value);
   }
 
   /**
    * Implement links as buttons to avoid any risk of loading the link in the
    * the panel.
    */
   renderLink(href, text) {
     return button(
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -83,16 +83,17 @@ ChromeUtils.defineModuleGetter(this, "Va
   "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
 
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/debugger-client", true);
 loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/environment-client");
 loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/object-client");
 loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice", true);
+loader.lazyRequireGetter(this, "openDocLink", "devtools/client/shared/link", true);
 
 XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () =>
   Services.prefs.getIntPref("devtools.debugger.remote-timeout"));
 
 ChromeUtils.defineModuleGetter(this, "ShortcutUtils",
   "resource://gre/modules/ShortcutUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "Reflect",
@@ -1982,18 +1983,17 @@ var Scratchpad = {
       }
     }
   },
 
   /**
    * Opens the MDN documentation page for Scratchpad.
    */
   openDocumentationPage: function SP_openDocumentationPage() {
-    const url = this.strings.GetStringFromName("help.openDocumentationPage");
-    this.browserWindow.openWebLinkIn(url, "tab");
+    openDocLink(this.strings.GetStringFromName("help.openDocumentationPage"));
     this.browserWindow.focus();
   },
 };
 
 /**
  * Represents the DebuggerClient connection to a specific tab as used by the
  * Scratchpad.
  *
--- a/devtools/client/shared/components/MdnLink.js
+++ b/devtools/client/shared/components/MdnLink.js
@@ -1,20 +1,19 @@
 /* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const { gDevTools } = require("devtools/client/framework/devtools");
+const { a } = dom;
 
-const { a } = dom;
+loader.lazyRequireGetter(this, "openDocLink", "devtools/client/shared/link", true);
 
 function MDNLink({ url, title }) {
   return (
     a({
       className: "devtools-button learn-more-link",
       title,
       onClick: (e) => onLearnMoreClick(e, url),
     })
@@ -25,20 +24,12 @@ MDNLink.displayName = "MDNLink";
 
 MDNLink.propTypes = {
   url: PropTypes.string.isRequired,
 };
 
 function onLearnMoreClick(e, url) {
   e.stopPropagation();
   e.preventDefault();
-
-  const win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-  const { button, ctrlKey, metaKey } = e;
-  const isOSX = Services.appinfo.OS == "Darwin";
-  let where = "tab";
-  if (button === 1 || (button === 0 && (isOSX ? metaKey : ctrlKey))) {
-    where = "tabshifted";
-  }
-  win.openWebLinkIn(url, where, {triggeringPrincipal: win.document.nodePrincipal});
+  openDocLink(url);
 }
 
 module.exports = MDNLink;
--- a/devtools/client/shared/link.js
+++ b/devtools/client/shared/link.js
@@ -1,45 +1,86 @@
 /* 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 { gDevTools } = require("devtools/client/framework/devtools");
+const { TargetFactory } = require("devtools/client/framework/target");
 
 /**
  * Retrieve the most recent chrome window.
  */
 function _getTopWindow() {
-  return Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+  // Try the main application window, such as a browser window.
+  let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+  if (win && win.openWebLinkIn && win.openTrustedLinkIn) {
+    return win;
+  }
+  // For non-browser cases like Browser Toolbox, try any chrome window.
+  win = Services.wm.getMostRecentWindow(null);
+  if (win && win.openWebLinkIn && win.openTrustedLinkIn) {
+    return win;
+  }
+  return null;
 }
 
 /**
- * Opens a |url| controlled by webcontent in a new tab.
+ * Opens a |url| that does not require trusted access, such as a documentation page, in a
+ * new tab.
  *
  * @param {String} url
  *        The url to open.
  * @param {Object} options
  *        Optional parameters, see documentation for openUILinkIn in utilityOverlay.js
  */
-exports.openWebLink = async function(url, options) {
+exports.openDocLink = async function(url, options) {
   const top = _getTopWindow();
-  if (top && typeof top.openWebLinkIn === "function") {
-    top.openWebLinkIn(url, "tab", options);
+  if (!top) {
+    return;
   }
+  top.openWebLinkIn(url, "tab", options);
+};
+
+/**
+ * Opens a |url| controlled by web content in a new tab.
+ *
+ * If the current tab has an open toolbox, this will attempt to refine the
+ * `triggeringPrincipal` of the link using the tab's `contentPrincipal`.  This is only an
+ * approximation, so bug 1467945 hopes to improve this.
+ *
+ * @param {String} url
+ *        The url to open.
+ * @param {Object} options
+ *        Optional parameters, see documentation for openUILinkIn in utilityOverlay.js
+ */
+exports.openContentLink = async function(url, options = {}) {
+  const top = _getTopWindow();
+  if (!top) {
+    return;
+  }
+  if (!options.triggeringPrincipal && top.gBrowser) {
+    const tab = top.gBrowser.selectedTab;
+    if (TargetFactory.isKnownTab(tab)) {
+      const target = TargetFactory.forTab(tab);
+      options.triggeringPrincipal = target.contentPrincipal;
+    }
+  }
+  top.openWebLinkIn(url, "tab", options);
 };
 
 /**
  * Open a trusted |url| in a new tab using the SystemPrincipal.
  *
  * @param {String} url
  *        The url to open.
  * @param {Object} options
  *        Optional parameters, see documentation for openUILinkIn in utilityOverlay.js
  */
 exports.openTrustedLink = async function(url, options) {
   const top = _getTopWindow();
-  if (top && typeof top.openTrustedLinkIn === "function") {
-    top.openTrustedLinkIn(url, "tab", options);
+  if (!top) {
+    return;
   }
+  top.openTrustedLinkIn(url, "tab", options);
 };
--- a/devtools/client/shared/test/browser_link.js
+++ b/devtools/client/shared/test/browser_link.js
@@ -1,30 +1,30 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-// Test link helpers openWebLink, openTrustedLink.
+// Test link helpers openDocLink, openTrustedLink.
 
 // Use any valid test page here.
 const TEST_URI = TEST_URI_ROOT + "dummy.html";
 
-const {openWebLink, openTrustedLink} =
+const {openDocLink, openTrustedLink} =
   require("devtools/client/shared/link");
 
 add_task(async function() {
   // Open a link to a page that will not trigger any request.
   info("Open web link to example.com test page");
-  openWebLink(TEST_URI);
+  openDocLink(TEST_URI);
   await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
 
   is(gBrowser.selectedBrowser.currentURI.spec, TEST_URI,
-    "openWebLink opened a tab with the expected url");
+    "openDocLink opened a tab with the expected url");
 
   info("Open trusted link to about:debugging");
   openTrustedLink("about:debugging");
   await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
 
   is(gBrowser.selectedBrowser.currentURI.spec, "about:debugging",
     "openTrustedLink opened a tab with the expected url");
 
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -23,16 +23,17 @@ const {SplitView} = require("resource://
 const {StyleSheetEditor} = require("resource://devtools/client/styleeditor/StyleSheetEditor.jsm");
 const {PluralForm} = require("devtools/shared/plural-form");
 const {PrefObserver} = require("devtools/client/shared/prefs");
 const csscoverage = require("devtools/shared/fronts/csscoverage");
 const {KeyCodes} = require("devtools/client/shared/keycodes");
 const {OriginalSource} = require("devtools/client/styleeditor/original-source");
 
 loader.lazyRequireGetter(this, "ResponsiveUIManager", "devtools/client/responsive.html/manager", true);
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
 
 const LOAD_ERROR = "error-load";
 const STYLE_EDITOR_TEMPLATE = "stylesheet";
 const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
 const PREF_MEDIA_SIDEBAR = "devtools.styleeditor.showMediaSidebar";
 const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.mediaSidebarWidth";
 const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
 const PREF_ORIG_SOURCES = "devtools.source-map.client-service.enabled";
@@ -482,17 +483,17 @@ StyleEditorUI.prototype = {
     }
   },
 
   /**
    * Open a particular stylesheet in a new tab.
    */
   _openLinkNewTab: function() {
     if (this._contextMenuStyleSheet) {
-      this._window.openWebLinkIn(this._contextMenuStyleSheet.href, "tab");
+      openContentLink(this._contextMenuStyleSheet.href);
     }
   },
 
   /**
    * Remove a particular stylesheet editor from the UI
    *
    * @param {StyleSheetEditor}  editor
    *        The editor to remove.
--- a/devtools/client/styleeditor/test/browser_styleeditor_opentab.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_opentab.js
@@ -16,21 +16,22 @@ add_task(async function() {
     "The menu item is not disabled");
   is(ui._openLinkNewTabItem.getAttribute("hidden"), "false",
     "The menu item is not hidden");
 
   const url = "https://example.com/browser/devtools/client/styleeditor/test/" +
     "simple.css";
   is(ui._contextMenuStyleSheet.href, url, "Correct URL for sheet");
 
-  const originalOpenWebLinkIn = ui._window.openWebLinkIn;
+  const browserWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+  const originalOpenWebLinkIn = browserWindow.openWebLinkIn;
   const tabOpenedDefer = new Promise(resolve => {
-    ui._window.openWebLinkIn = newUrl => {
+    browserWindow.openWebLinkIn = newUrl => {
       // Reset the actual openWebLinkIn function before proceeding.
-      ui._window.openWebLinkIn = originalOpenWebLinkIn;
+      browserWindow.openWebLinkIn = originalOpenWebLinkIn;
 
       is(newUrl, url, "The correct tab has been opened");
       resolve();
     };
   });
 
   ui._openLinkNewTabItem.click();
 
--- a/devtools/client/webconsole/hudservice.js
+++ b/devtools/client/webconsole/hudservice.js
@@ -11,16 +11,17 @@ loader.lazyRequireGetter(this, "TargetFa
 loader.lazyRequireGetter(this, "Tools", "devtools/client/definitions", true);
 loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
 loader.lazyRequireGetter(this, "WebConsoleFrame", "devtools/client/webconsole/webconsole-frame", true);
 loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/debugger-client", true);
 loader.lazyRequireGetter(this, "viewSource", "devtools/client/shared/view-source");
 loader.lazyRequireGetter(this, "l10n", "devtools/client/webconsole/webconsole-l10n");
+loader.lazyRequireGetter(this, "openDocLink", "devtools/client/shared/link", true);
 const BC_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
 
 // The preference prefix for all of the Browser Console filters.
 const BC_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter.";
 
 var gHudId = 0;
 
 function HUD_SERVICE() {
@@ -359,22 +360,17 @@ WebConsole.prototype = {
 
   /**
    * Open a link in a new tab.
    *
    * @param string link
    *        The URL you want to open in a new tab.
    */
   openLink(link, e) {
-    const isOSX = Services.appinfo.OS == "Darwin";
-    let where = "tab";
-    if (e && (e.button === 1 || (e.button === 0 && (isOSX ? e.metaKey : e.ctrlKey)))) {
-      where = "tabshifted";
-    }
-    this.chromeUtilsWindow.openWebLinkIn(link, where);
+    openDocLink(link);
   },
 
   /**
    * Open a link in Firefox's view source.
    *
    * @param string sourceURL
    *        The URL of the file.
    * @param integer sourceLine
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_allow_mixedcontent_securityerrors.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_allow_mixedcontent_securityerrors.js
@@ -47,28 +47,15 @@ add_task(async function() {
 
   const checkLink = ({ link, where, expectedLink, expectedTab }) => {
     is(link, expectedLink, `Clicking the provided link opens ${link}`);
     is(where, expectedTab, `Clicking the provided link opens in expected tab`);
   };
 
   info("Clicking on the Learn More link");
   const learnMoreLink = mixedActiveContentMessage.querySelector(".learn-more-link");
-  let linkSimulation = await simulateLinkClick(learnMoreLink);
+  const linkSimulation = await simulateLinkClick(learnMoreLink);
   checkLink({
     ...linkSimulation,
     expectedLink: LEARN_MORE_URI,
     expectedTab: "tab"
   });
-
-  const isOSX = Services.appinfo.OS == "Darwin";
-  const ctrlOrCmdKeyMouseEvent = new MouseEvent("click", {
-    bubbles: true,
-    [isOSX ? "metaKey" : "ctrlKey"]: true,
-    view: window
-  });
-  linkSimulation = await simulateLinkClick(learnMoreLink, ctrlOrCmdKeyMouseEvent);
-  checkLink({
-    ...linkSimulation,
-    expectedLink: LEARN_MORE_URI,
-    expectedTab: "tabshifted"
-  });
 });
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_network_messages_status_code.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_network_messages_status_code.js
@@ -35,26 +35,22 @@ add_task(async function task() {
 
   const xhrUrl = TEST_PATH + "test-data.json";
   const messageNode = await waitFor(() => findMessage(hud, xhrUrl));
   const statusCodeNode = messageNode.querySelector(".status-code");
   info("Network message found.");
 
   ok(statusCodeNode.title, l10n.getStr("webConsoleMoreInfoLabel"));
   const {
-    middleMouseEvent,
-    ctrlOrCmdKeyMouseEvent,
     rightClickMouseEvent,
     rightClickCtrlOrCmdKeyMouseEvent,
   } = getMouseEvents();
 
   const testCases = [
-    { clickEvent: middleMouseEvent, link: LEARN_MORE_URI, where: "tabshifted" },
     { clickEvent: null, link: LEARN_MORE_URI, where: "tab" },
-    { clickEvent: ctrlOrCmdKeyMouseEvent, link: LEARN_MORE_URI, where: "tabshifted" },
     { clickEvent: rightClickMouseEvent, link: null, where: null },
     { clickEvent: rightClickCtrlOrCmdKeyMouseEvent, link: null, where: null }
   ];
 
   for (const testCase of testCases) {
     const { clickEvent } = testCase;
     const onConsoleMenuOpened = [
       rightClickMouseEvent,
@@ -70,37 +66,25 @@ add_task(async function task() {
       await onConsoleMenuOpened;
     }
   }
 });
 
 function getMouseEvents() {
   const isOSX = Services.appinfo.OS == "Darwin";
 
-  const middleMouseEvent = new MouseEvent("click", {
-    bubbles: true,
-    button: 1,
-    view: window
-  });
-  const ctrlOrCmdKeyMouseEvent = new MouseEvent("click", {
-    bubbles: true,
-    [isOSX ? "metaKey" : "ctrlKey"]: true,
-    view: window
-  });
   const rightClickMouseEvent = new MouseEvent("contextmenu", {
     bubbles: true,
     button: 2,
     view: window
   });
   const rightClickCtrlOrCmdKeyMouseEvent = new MouseEvent("contextmenu", {
     bubbles: true,
     button: 2,
     [isOSX ? "metaKey" : "ctrlKey"]: true,
     view: window
   });
 
   return {
-    middleMouseEvent,
-    ctrlOrCmdKeyMouseEvent,
     rightClickMouseEvent,
     rightClickCtrlOrCmdKeyMouseEvent,
   };
 }
--- a/devtools/client/webconsole/test/mochitest/head.js
+++ b/devtools/client/webconsole/test/mochitest/head.js
@@ -490,42 +490,45 @@ async function closeConsole(tab = gBrows
  *          A Promise that is resolved when the link click simulation occured or
  *          when the click is not dispatched.
  *          The promise resolves with an object that holds the following properties
  *          - link: url of the link or null(if event not fired)
  *          - where: "tab" if tab is active or "tabshifted" if tab is inactive
  *            or null(if event not fired)
  */
 function simulateLinkClick(element, clickEventProps) {
+  const browserWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+
   // Override LinkIn methods to prevent navigating.
-  const oldOpenTrustedLinkIn = window.openTrustedLinkIn;
-  const oldOpenWebLinkIn = window.openWebLinkIn;
+  const oldOpenTrustedLinkIn = browserWindow.openTrustedLinkIn;
+  const oldOpenWebLinkIn = browserWindow.openWebLinkIn;
 
   const onOpenLink = new Promise((resolve) => {
-    window.openWebLinkIn = window.openTrustedLinkIn = function(link, where) {
-      window.openTrustedLinkIn = oldOpenTrustedLinkIn;
-      window.openWebLinkIn = oldOpenWebLinkIn;
+    const openLinkIn = function(link, where) {
+      browserWindow.openTrustedLinkIn = oldOpenTrustedLinkIn;
+      browserWindow.openWebLinkIn = oldOpenWebLinkIn;
       resolve({link: link, where});
     };
+    browserWindow.openWebLinkIn = browserWindow.openTrustedLinkIn = openLinkIn;
     if (clickEventProps) {
       // Click on the link using the event properties.
       element.dispatchEvent(clickEventProps);
     } else {
       // Click on the link.
       element.click();
     }
   });
 
   // Declare a timeout Promise that we can use to make sure openTrustedLinkIn or
   // openWebLinkIn was not called.
   let timeoutId;
   const onTimeout = new Promise(function(resolve) {
     timeoutId = setTimeout(() => {
-      window.openTrustedLinkIn = oldOpenTrustedLinkIn;
-      window.openWebLinkIn = oldOpenWebLinkIn;
+      browserWindow.openTrustedLinkIn = oldOpenTrustedLinkIn;
+      browserWindow.openWebLinkIn = oldOpenWebLinkIn;
       timeoutId = null;
       resolve({link: null, where: null});
     }, 1000);
   });
 
   onOpenLink.then(() => {
     if (timeoutId) {
       clearTimeout(timeoutId);
--- a/devtools/client/webconsole/utils/context-menu.js
+++ b/devtools/client/webconsole/utils/context-menu.js
@@ -1,27 +1,26 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 {gDevTools} = require("devtools/client/framework/devtools");
-
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 
 const { MESSAGE_SOURCE } = require("devtools/client/webconsole/constants");
 
 const clipboardHelper = require("devtools/shared/platform/clipboard");
 const { l10n } = require("devtools/client/webconsole/utils/messages");
 
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
+
 /**
  * Create a Menu instance for the webconsole.
  *
  * @param {Object} hud
  *        The webConsoleFrame.
  * @param {Element} parentNode
  *        The container of the new console frontend output wrapper.
  * @param {Object} options
@@ -84,20 +83,17 @@ function createContextMenu(hud, parentNo
     id: "console-menu-open-url",
     label: l10n.getStr("webconsole.menu.openURL.label"),
     accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
     visible: source === MESSAGE_SOURCE.NETWORK,
     click: () => {
       if (!request) {
         return;
       }
-      const mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-      mainWindow.openWebLinkIn(request.url, "tab", {
-        triggeringPrincipal: mainWindow.document.nodePrincipal,
-      });
+      openContentLink(request.url);
     },
   }));
 
   // Store as global variable.
   menu.append(new MenuItem({
     id: "console-menu-store",
     label: l10n.getStr("webconsole.menu.storeAsGlobalVar.label"),
     accesskey: l10n.getStr("webconsole.menu.storeAsGlobalVar.accesskey"),
--- a/devtools/client/webide/content/webide.js
+++ b/devtools/client/webide/content/webide.js
@@ -15,16 +15,17 @@ const {AppProjects} = require("devtools/
 const {Connection} = require("devtools/shared/client/connection-manager");
 const {AppManager} = require("devtools/client/webide/modules/app-manager");
 const EventEmitter = require("devtools/shared/event-emitter");
 const promise = require("promise");
 const {GetAvailableAddons} = require("devtools/client/webide/modules/addons");
 const {getJSON} = require("devtools/client/shared/getjson");
 const Telemetry = require("devtools/client/shared/telemetry");
 const {RuntimeScanners} = require("devtools/client/webide/modules/runtimes");
+const {openContentLink} = require("devtools/client/shared/link");
 
 const Strings =
   Services.strings.createBundle("chrome://devtools/locale/webide.properties");
 
 const TELEMETRY_WEBIDE_IMPORT_PROJECT_COUNT = "DEVTOOLS_WEBIDE_IMPORT_PROJECT_COUNT";
 
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
 
@@ -172,24 +173,17 @@ var UI = {
       case "runtime-targets":
         this.autoSelectProject();
         break;
     }
     this._updatePromise = promise.resolve();
   },
 
   openInBrowser: function(url) {
-    // Open a URL in a Firefox window
-    const mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-    if (mainWindow) {
-      mainWindow.openWebLinkIn(url, "tab");
-      mainWindow.focus();
-    } else {
-      window.open(url);
-    }
+    openContentLink(url);
   },
 
   updateTitle: function() {
     const project = AppManager.selectedProject;
     if (project) {
       window.document.title = Strings.formatStringFromName("title_app", [project.name], 1);
     } else {
       window.document.title = Strings.GetStringFromName("title_noApp");
--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/shared/gcli/commands/screenshot.js
@@ -8,16 +8,18 @@ const { Cc, Ci, Cr, Cu } = require("chro
 const ChromeUtils = require("ChromeUtils");
 const l10n = require("gcli/l10n");
 const Services = require("Services");
 const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
 const { getRect } = require("devtools/shared/layout/utils");
 const defer = require("devtools/shared/defer");
 const { Task } = require("devtools/shared/task");
 
+loader.lazyRequireGetter(this, "openContentLink", "devtools/client/shared/link", true);
+
 loader.lazyImporter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
 loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
 loader.lazyImporter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
 loader.lazyImporter(this, "PrivateBrowsingUtils",
                           "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 // String used as an indication to generate default file name in the following
 // format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
@@ -155,20 +157,17 @@ exports.items = [
         root.appendChild(image);
       }
 
       // Click handler
       if (imageSummary.href || imageSummary.filename) {
         root.style.cursor = "pointer";
         root.addEventListener("click", () => {
           if (imageSummary.href) {
-            const mainWindow = context.environment.chromeWindow;
-            mainWindow.openWebLinkIn(imageSummary.href, "tab", {
-              triggeringPrincipal: document.nodePrincipal,
-            });
+            openContentLink(imageSummary.href);
           } else if (imageSummary.filename) {
             const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
             file.initWithPath(imageSummary.filename);
             file.reveal();
           }
         });
       }