Merge mozilla-central to autoland. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Tue, 10 Apr 2018 19:56:38 +0300
changeset 412690 98d330009c839f8d4d67d84589b1a07a8497265d
parent 412689 d34178082ca879e801132c333de935de37280e1e (current diff)
parent 412606 0a2dae2d8cf9f628c55668514c54a23da446d5de (diff)
child 412691 53109cc01f60ddbfb7e85d47cc49c3ff92d894de
push id101981
push useraiakab@mozilla.com
push dateTue, 10 Apr 2018 22:18:59 +0000
treeherdermozilla-inbound@9ad2b8aabfae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. a=merge CLOSED TREE
--- a/browser/base/content/browser-feeds.js
+++ b/browser/base/content/browser-feeds.js
@@ -209,17 +209,20 @@ var FeedHandler = {
     urlSecurityCheck(href, gBrowser.contentPrincipal,
                      Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
     this.loadFeed(href, event);
   },
 
   loadFeed(href, event) {
     let feeds = gBrowser.selectedBrowser.feeds;
     try {
-      openUILink(href, event, { ignoreAlt: true });
+      openUILink(href, event, {
+        ignoreAlt: true,
+        triggeringPrincipal: gBrowser.contentPrincipal,
+      });
     } finally {
       // We might default to a livebookmarks modal dialog,
       // so reset that if the user happens to click it again
       gBrowser.selectedBrowser.feeds = feeds;
     }
   },
 
   get _feedMenuitem() {
--- a/browser/base/content/browser-media.js
+++ b/browser/base/content/browser-media.js
@@ -315,17 +315,17 @@ let gDecoderDoctorHandler = {
               Services.prefs.getBoolPref(buttonClickedPref, false);
             if (!clickedInPref) {
               Services.prefs.setBoolPref(buttonClickedPref, true);
               histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED_FIRST);
             }
             histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED);
 
             let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
-            openUILinkIn(baseURL + sumo, "tab");
+            openTrustedLinkIn(baseURL + sumo, "tab");
           }
         });
       }
       let endpoint = gDecoderDoctorHandler.getEndpointForReportIssueButton(type);
       if (endpoint) {
         buttons.push({
           label: gNavigatorBundle.getString("decoder.decodeError.button"),
           accessKey: gNavigatorBundle.getString("decoder.decodeError.accesskey"),
@@ -345,17 +345,17 @@ let gDecoderDoctorHandler = {
             params.append("src", "media-decode-error");
 
             let details = {"Technical Information:": decodeIssue};
             if (resourceURL) {
               details["Resource:"] = resourceURL;
             }
 
             params.append("details", JSON.stringify(details));
-            openUILinkIn(endpoint + "?" + params.toString(), "tab");
+            openTrustedLinkIn(endpoint + "?" + params.toString(), "tab");
           }
         });
       }
 
       box.appendNotification(
           title,
           notificationId,
           "", // This uses the info icon as specified below.
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -718,17 +718,20 @@ HistoryMenu.prototype = {
     this.toggleTabsFromOtherComputers();
   },
 
   _onCommand: function HM__onCommand(aEvent) {
     let placesNode = aEvent.target._placesNode;
     if (placesNode) {
       if (!PrivateBrowsingUtils.isWindowPrivate(window))
         PlacesUIUtils.markPageAsTyped(placesNode.uri);
-      openUILink(placesNode.uri, aEvent, { ignoreAlt: true });
+      openUILink(placesNode.uri, aEvent, {
+        ignoreAlt: true,
+        triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+      });
     }
   }
 };
 
 /**
  * Functions for handling events in the Bookmarks Toolbar and menu.
  */
 var BookmarksEventHandler = {
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -93,17 +93,17 @@ var gPluginHandler = {
 
   // Callback for user clicking on the link in a click-to-play plugin
   // (where the plugin has an update)
   openPluginUpdatePage(pluginTag) {
     let url = Services.blocklist.getPluginInfoURL(pluginTag);
     if (!url) {
       url = Services.blocklist.getPluginBlocklistURL(pluginTag);
     }
-    openUILinkIn(url, "tab");
+    openTrustedLinkIn(url, "tab");
   },
 
   submitReport: function submitReport(runID, keyVals, submitURLOptIn) {
     if (!AppConstants.MOZ_CRASHREPORTER) {
       return;
     }
     Services.prefs.setBoolPref("dom.ipc.plugins.reportCrashURL", submitURLOptIn);
     PluginCrashReporter.submitCrashReport(runID, keyVals);
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -305,17 +305,17 @@ var gSync = {
     switchToTabHavingURI(url, true, {
       replaceQueryString: true,
       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
     });
   },
 
   async openConnectAnotherDevice(entryPoint) {
     const url = await FxAccounts.config.promiseConnectDeviceURI(entryPoint);
-    openUILinkIn(url, "tab");
+    openTrustedLinkIn(url, "tab");
   },
 
   openSendToDevicePromo() {
     let url = this.PRODUCT_INFO_BASE_URL;
     url += "send-tabs/?utm_source=" + Services.appinfo.name.toLowerCase();
     switchToTabHavingURI(url, true, { replaceQueryString: true });
   },
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -909,17 +909,17 @@ function gKeywordURIFixup({ target: brow
         label: yesMessage,
         accessKey: gNavigatorBundle.getString("keywordURIFixup.goTo.accesskey"),
         callback() {
           // Do not set this preference while in private browsing.
           if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
             let pref = "browser.fixup.domainwhitelist." + asciiHost;
             Services.prefs.setBoolPref(pref, true);
           }
-          openUILinkIn(alternativeURI.spec, "current");
+          openTrustedLinkIn(alternativeURI.spec, "current");
         }
       },
       {
         label: gNavigatorBundle.getString("keywordURIFixup.dismiss"),
         accessKey: gNavigatorBundle.getString("keywordURIFixup.dismiss.accesskey"),
         callback() {
           let notification = notificationBox.getNotificationWithValue("keyword-uri-fixup");
           notificationBox.removeNotification(notification, true);
@@ -2216,17 +2216,17 @@ function BrowserGoHome(aEvent) {
   var urls;
 
   // Home page should open in a new tab when current tab is an app tab
   if (where == "current" &&
       gBrowser &&
       gBrowser.selectedTab.pinned)
     where = "tab";
 
-  // openUILinkIn in utilityOverlay.js doesn't handle loading multiple pages
+  // openTrustedLinkIn in utilityOverlay.js doesn't handle loading multiple pages
   switch (where) {
   case "current":
     loadOneOrMoreURIs(homePage, Services.scriptSecurityManager.getSystemPrincipal());
     gBrowser.selectedBrowser.focus();
     break;
   case "tabshifted":
   case "tab":
     urls = homePage.split("|");
@@ -2339,17 +2339,19 @@ function BrowserOpenTab(event) {
         relatedToCurrent = true;
         break;
       case "current":
         where = "tab";
         break;
     }
   }
 
-  openUILinkIn(BROWSER_NEW_TAB_URL, where, { relatedToCurrent });
+  openTrustedLinkIn(BROWSER_NEW_TAB_URL, where, {
+    relatedToCurrent,
+  });
 }
 
 var gLastOpenDirectory = {
   _lastDir: null,
   get path() {
     if (!this._lastDir || !this._lastDir.exists()) {
       try {
         this._lastDir = Services.prefs.getComplexValue("browser.open.lastDir",
@@ -2388,17 +2390,17 @@ function BrowserOpenFileWindow() {
       if (aResult == nsIFilePicker.returnOK) {
         try {
           if (fp.file) {
             gLastOpenDirectory.path =
               fp.file.parent.QueryInterface(Ci.nsIFile);
           }
         } catch (ex) {
         }
-        openUILinkIn(fp.fileURL.spec, "current");
+        openTrustedLinkIn(fp.fileURL.spec, "current");
       }
     };
 
     fp.init(window, gNavigatorBundle.getString("openFile"),
             nsIFilePicker.modeOpen);
     fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText |
                      nsIFilePicker.filterImages | nsIFilePicker.filterXML |
                      nsIFilePicker.filterHTML);
@@ -3183,31 +3185,31 @@ var BrowserOnClick = {
       title = gNavigatorBundle.getString("safebrowsing.reportedAttackSite");
       // There's no button if we can not get report url, for example if the provider
       // of blockedInfo is not Google
       if (reportUrl) {
         buttons[1] = {
           label: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.label"),
           accessKey: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.accessKey"),
           callback() {
-            openUILinkIn(reportUrl, "tab");
+            openTrustedLinkIn(reportUrl, "tab");
           }
         };
       }
     } else if (reason === "phishing") {
       let reportUrl = gSafeBrowsing.getReportURL("PhishMistake", blockedInfo);
       title = gNavigatorBundle.getString("safebrowsing.deceptiveSite");
       // There's no button if we can not get report url, for example if the provider
       // of blockedInfo is not Google
       if (reportUrl) {
         buttons[1] = {
           label: gNavigatorBundle.getString("safebrowsing.notADeceptiveSiteButton.label"),
           accessKey: gNavigatorBundle.getString("safebrowsing.notADeceptiveSiteButton.accessKey"),
           callback() {
-            openUILinkIn(reportUrl, "tab");
+            openTrustedLinkIn(reportUrl, "tab");
           }
         };
       }
     } else if (reason === "unwanted") {
       title = gNavigatorBundle.getString("safebrowsing.reportedUnwantedSite");
       // There is no button for reporting errors since Google doesn't currently
       // provide a URL endpoint for these reports.
     } else if (reason === "harmful") {
@@ -4021,17 +4023,17 @@ const BrowserSearch = {
 
   get searchEnginesURL() {
     return formatURL("browser.search.searchEnginesURL", true);
   },
 
   loadAddEngines: function BrowserSearch_loadAddEngines() {
     var newWindowPref = Services.prefs.getIntPref("browser.link.open_newwindow");
     var where = newWindowPref == 3 ? "tab" : "window";
-    openUILinkIn(this.searchEnginesURL, where);
+    openTrustedLinkIn(this.searchEnginesURL, where);
   },
 
   _getSearchEngineId(engine) {
     if (engine && engine.identifier) {
       return engine.identifier;
     }
 
     if (!engine || (engine.name === undefined))
@@ -4219,17 +4221,17 @@ function addToUrlbarHistory(aUrlToAdd) {
       aUrlToAdd &&
       !aUrlToAdd.includes(" ") &&
       !/[\x00-\x1F]/.test(aUrlToAdd)) // eslint-disable-line no-control-regex
     PlacesUIUtils.markPageAsTyped(aUrlToAdd);
 }
 
 function BrowserDownloadsUI() {
   if (PrivateBrowsingUtils.isWindowPrivate(window)) {
-    openUILinkIn("about:downloads", "tab");
+    openTrustedLinkIn("about:downloads", "tab");
   } else {
     PlacesCommandHook.showPlacesOrganizer("Downloads");
   }
 }
 
 function toOpenWindowByType(inType, uri, features) {
   var topWindow = Services.wm.getMostRecentWindow(inType);
 
@@ -4389,17 +4391,17 @@ function updateEditUIVisibility() {
  * Opens a new tab with the userContextId specified as an attribute of
  * sourceEvent. This attribute is propagated to the top level originAttributes
  * living on the tab's docShell.
  *
  * @param event
  *        A click event on a userContext File Menu option
  */
 function openNewUserContextTab(event) {
-  openUILinkIn(BROWSER_NEW_TAB_URL, "tab", {
+  openTrustedLinkIn(BROWSER_NEW_TAB_URL, "tab", {
     userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
   });
 }
 
 /**
  * Updates File Menu User Context UI visibility depending on
  * privacy.userContext.enabled pref state.
  */
@@ -6159,17 +6161,19 @@ function middleMousePaste(event) {
       // but don't let that interfere with the loading of the url.
       Cu.reportError(ex);
     }
 
     if (where != "current" ||
         lastLocationChange == gBrowser.selectedBrowser.lastLocationChange) {
       openUILink(data.url, event,
                  { ignoreButton: true,
-                   disallowInheritPrincipal: !data.mayInheritPrincipal });
+                   disallowInheritPrincipal: !data.mayInheritPrincipal,
+                   triggeringPrincipal: gBrowser.selectedBrowser.contentPrincipal,
+                 });
     }
   });
 
   if (event instanceof Event) {
     event.stopPropagation();
   }
 }
 
@@ -7095,17 +7099,17 @@ function BrowserOpenAddonsMgr(aView) {
       return;
     }
 
     // This must be a new load, else the ping/pong would have
     // found the window above.
     let whereToOpen = (window.gBrowser && isTabEmpty(gBrowser.selectedTab)) ?
                       "current" :
                       "tab";
-    openUILinkIn("about:addons", whereToOpen);
+    openTrustedLinkIn("about:addons", whereToOpen);
 
     Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
       Services.obs.removeObserver(observer, aTopic);
       if (aView) {
         aSubject.loadView(aView);
       }
       aSubject.QueryInterface(Ci.nsIDOMWindow);
       aSubject.focus();
@@ -7271,17 +7275,17 @@ function ReportFalseDeceptiveSite() {
     docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked");
 
   if (isPhishingPage) {
     let mm = gBrowser.selectedBrowser.messageManager;
     let onMessage = (message) => {
       mm.removeMessageListener("DeceptiveBlockedDetails:Result", onMessage);
       let reportUrl = gSafeBrowsing.getReportURL("PhishMistake", message.data.blockedInfo);
       if (reportUrl) {
-        openUILinkIn(reportUrl, "tab");
+        openTrustedLinkIn(reportUrl, "tab");
       } else {
         let bundle =
           Services.strings.createBundle("chrome://browser/locale/safebrowsing/safebrowsing.properties");
         Services.prompt.alert(window,
                               bundle.GetStringFromName("errorReportFalseDeceptiveTitle"),
                               bundle.formatStringFromName("errorReportFalseDeceptiveMessage",
                                                           [message.data.blockedInfo.provider], 1));
         }
@@ -8570,18 +8574,18 @@ var gPrivateBrowsingUI = {
  *
  * @param aURI
  *        URI to search for
  * @param aOpenNew
  *        True to open a new tab and switch to it, if no existing tab is found.
  *        If no suitable window is found, a new one will be opened.
  * @param aOpenParams
  *        If switching to this URI results in us opening a tab, aOpenParams
- *        will be the parameter object that gets passed to openUILinkIn. Please
- *        see the documentation for openUILinkIn to see what parameters can be
+ *        will be the parameter object that gets passed to openTrustedLinkIn. Please
+ *        see the documentation for openTrustedLinkIn to see what parameters can be
  *        passed via this object.
  *        This object also allows:
  *        - 'ignoreFragment' property to be set to true to exclude fragment-portion
  *        matching when comparing URIs.
  *          If set to "whenComparing", the fragment will be unmodified.
  *          If set to "whenComparingAndReplace", the fragment will be replaced.
  *        - 'ignoreQueryString' boolean property to be set to true to exclude query string
  *        matching when comparing URIs.
@@ -8697,19 +8701,19 @@ function switchToTabHavingURI(aURI, aOpe
       continue;
     if (switchIfURIInWindow(browserWin))
       return true;
   }
 
   // No opened tab has that url.
   if (aOpenNew) {
     if (isBrowserWindow && isTabEmpty(gBrowser.selectedTab))
-      openUILinkIn(aURI.spec, "current", aOpenParams);
+      openTrustedLinkIn(aURI.spec, "current", aOpenParams);
     else
-      openUILinkIn(aURI.spec, "tab", aOpenParams);
+      openTrustedLinkIn(aURI.spec, "tab", aOpenParams);
   }
 
   return false;
 }
 
 var RestoreLastSessionObserver = {
   init() {
     if (SessionStore.canRestoreLastSession &&
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -779,30 +779,27 @@ nsContextMenu.prototype = {
       params.noReferrer = true;
     }
 
     return params;
   },
 
   // Open linked-to URL in a new window.
   openLink() {
-    urlSecurityCheck(this.linkURL, this.principal);
     openLinkIn(this.linkURL, "window", this._openLinkInParameters());
   },
 
   // Open linked-to URL in a new private window.
   openLinkInPrivateWindow() {
-    urlSecurityCheck(this.linkURL, this.principal);
     openLinkIn(this.linkURL, "window",
                this._openLinkInParameters({ private: true }));
   },
 
   // Open linked-to URL in a new tab.
   openLinkInTab(event) {
-    urlSecurityCheck(this.linkURL, this.principal);
     let referrerURI = gContextMenuContentData.documentURIObject;
 
     // if its parent allows mixed content and the referring URI passes
     // a same origin check with the target URI, we can preserve the users
     // decision of disabling MCB on a page for it's child tabs.
     let persistAllowMixedContentInChildTab = false;
 
     if (gContextMenuContentData.parentAllowsMixedContent) {
@@ -819,17 +816,16 @@ nsContextMenu.prototype = {
       userContextId: parseInt(event.target.getAttribute("data-usercontextid")),
     };
 
     openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params));
   },
 
   // open URL in current tab
   openLinkInCurrent() {
-    urlSecurityCheck(this.linkURL, this.principal);
     openLinkIn(this.linkURL, "current", this._openLinkInParameters());
   },
 
   // Open frame in a new tab.
   openFrameInTab() {
     let referrer = gContextMenuContentData.referrer;
     openLinkIn(gContextMenuContentData.docLocation, "tab",
                { charset: gContextMenuContentData.charSet,
@@ -848,23 +844,22 @@ nsContextMenu.prototype = {
     let referrer = gContextMenuContentData.referrer;
     openLinkIn(gContextMenuContentData.docLocation, "window",
                { charset: gContextMenuContentData.charSet,
                  referrerURI: referrer ? makeURI(referrer) : null });
   },
 
   // Open clicked-in frame in the same window.
   showOnlyThisFrame() {
-    urlSecurityCheck(gContextMenuContentData.docLocation,
-                     this.browser.contentPrincipal,
-                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     let referrer = gContextMenuContentData.referrer;
-    openUILinkIn(gContextMenuContentData.docLocation, "current",
-                 { disallowInheritPrincipal: true,
-                   referrerURI: referrer ? makeURI(referrer) : null });
+    openWebLinkIn(gContextMenuContentData.docLocation, "current", {
+      disallowInheritPrincipal: true,
+      referrerURI: referrer ? makeURI(referrer) : null,
+      triggeringPrincipal: this.browser.contentPrincipal,
+    });
   },
 
   reload(event) {
     BrowserReloadOrDuplicate(event);
   },
 
   // View Partial Source
   viewPartialSource(aContext) {
@@ -907,33 +902,28 @@ nsContextMenu.prototype = {
   },
 
   viewImageInfo() {
     BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab",
                     this.imageInfo, null, this.browser);
   },
 
   viewImageDesc(e) {
-    urlSecurityCheck(this.imageDescURL,
-                     this.browser.contentPrincipal,
-                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true,
-                                       referrerURI: gContextMenuContentData.documentURIObject });
+                                       referrerURI: gContextMenuContentData.documentURIObject,
+                                       triggeringPrincipal: this.principal,
+    });
   },
 
   viewFrameInfo() {
     BrowserPageInfo(gContextMenuContentData.docLocation, null, null,
                     this.frameOuterWindowID, this.browser);
   },
 
   reloadImage() {
-    urlSecurityCheck(this.mediaURL,
-                     this.browser.contentPrincipal,
-                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
-
     this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadImage",
                                                  null, { target: this.target });
   },
 
   _canvasToBlobURL(target) {
     let mm = this.browser.messageManager;
     return new Promise(function(resolve) {
       mm.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL", {}, { target });
@@ -952,22 +942,21 @@ nsContextMenu.prototype = {
     let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
     if (this.onCanvas) {
       this._canvasToBlobURL(this.target).then(function(blobURL) {
         openUILink(blobURL, e, { disallowInheritPrincipal: true,
                                  referrerURI,
                                  triggeringPrincipal: systemPrincipal});
       }, Cu.reportError);
     } else {
-      urlSecurityCheck(this.mediaURL,
-                       this.browser.contentPrincipal,
-                       Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
       openUILink(this.mediaURL, e, { disallowInheritPrincipal: true,
                                      referrerURI,
-                                     forceAllowDataURI: true });
+                                     forceAllowDataURI: true,
+                                     triggeringPrincipal: this.browser.contentPrincipal
+      });
     }
   },
 
   saveVideoFrameAsImage() {
     let mm = this.browser.messageManager;
     let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser);
 
     let name = "";
@@ -997,21 +986,20 @@ nsContextMenu.prototype = {
   },
 
   leaveDOMFullScreen() {
     document.exitFullscreen();
   },
 
   // Change current window to the URL of the background image.
   viewBGImage(e) {
-    urlSecurityCheck(this.bgImageURL,
-                     this.browser.contentPrincipal,
-                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
-                                     referrerURI: gContextMenuContentData.documentURIObject });
+                                     referrerURI: gContextMenuContentData.documentURIObject,
+                                     triggeringPrincipal: this.browser.contentPrincipal
+    });
   },
 
   setDesktopBackground() {
     let mm = this.browser.messageManager;
 
     mm.sendAsyncMessage("ContextMenu:SetAsDesktopBackground", null,
                         { target: this.target });
 
@@ -1196,18 +1184,16 @@ nsContextMenu.prototype = {
                            timer.TYPE_ONE_SHOT);
 
     // kick off the channel with our proxy object as the listener
     channel.asyncOpen2(new saveAsListener());
   },
 
   // Save URL of clicked-on link.
   saveLink() {
-    urlSecurityCheck(this.linkURL, this.principal);
-
     let isContentWindowPrivate = this.isRemote ? this.ownerDoc.isPrivate : undefined;
     this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc,
                     gContextMenuContentData.documentURIObject,
                     this.frameOuterWindowID,
                     this.linkDownload,
                     isContentWindowPrivate);
   },
 
@@ -1231,17 +1217,16 @@ nsContextMenu.prototype = {
                      isPrivate);
       }, Cu.reportError);
     } else if (this.onImage) {
       urlSecurityCheck(this.mediaURL, this.principal);
       saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
                    false, referrerURI, null, gContextMenuContentData.contentType,
                    gContextMenuContentData.contentDisposition, isPrivate);
     } else if (this.onVideo || this.onAudio) {
-      urlSecurityCheck(this.mediaURL, this.principal);
       var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
       this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
                       this.frameOuterWindowID, "", isContentWindowPrivate);
     }
   },
 
   // Backwards-compatibility wrapper
   sendImage() {
@@ -1401,17 +1386,17 @@ nsContextMenu.prototype = {
       version = Services.appinfo.version;
     } catch (e) { }
 
     uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version);
 
     var newWindowPref = Services.prefs.getIntPref("browser.link.open_newwindow");
     var where = newWindowPref == 3 ? "tab" : "window";
 
-    openUILinkIn(uri, where);
+    openTrustedLinkIn(uri, where);
   },
 
   bookmarkThisPage: function CM_bookmarkThisPage() {
     window.top.PlacesCommandHook
               .bookmarkPage(this.browser, true)
               .catch(Cu.reportError);
   },
 
@@ -1471,17 +1456,17 @@ nsContextMenu.prototype = {
   drmLearnMore(aEvent) {
     let drmInfoURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content";
     let dest = whereToOpenLink(aEvent);
     // Don't ever want this to open in the same tab as it'll unload the
     // DRM'd video, which is going to be a bad idea in most cases.
     if (dest == "current") {
       dest = "tab";
     }
-    openUILinkIn(drmInfoURL, dest);
+    openTrustedLinkIn(drmInfoURL, dest);
   },
 
   get imageURL() {
     if (this.onImage)
       return this.mediaURL;
     return "";
   },
 
--- a/browser/base/content/pageinfo/feeds.js
+++ b/browser/base/content/pageinfo/feeds.js
@@ -46,15 +46,15 @@ function addRow(name, type, url) {
   urlLabel.textContent = url;
   urlLabel.setAttribute("tooltiptext", url);
   urlLabel.addEventListener("click", ev => openUILink(this.value, ev));
   urlContainer.appendChild(urlLabel);
 
   const subscribeButton = document.createElement("button");
   subscribeButton.className = "feed-subscribe";
   subscribeButton.addEventListener("click",
-    () => openUILinkIn(url, "current", { ignoreAlt: true }));
+    () => openWebLinkIn(url, "current", { ignoreAlt: true }));
   subscribeButton.setAttribute("label", gBundle.getString("feedSubscribe"));
   subscribeButton.setAttribute("accesskey", gBundle.getString("feedSubscribe.accesskey"));
   bottom.appendChild(subscribeButton);
 
   document.getElementById("feedListbox").appendChild(item);
 }
--- a/browser/base/content/test/general/browser_bug579872.js
+++ b/browser/base/content/test/general/browser_bug579872.js
@@ -6,23 +6,23 @@ function test() {
   let newTab = BrowserTestUtils.addTab(gBrowser);
   waitForExplicitFinish();
   BrowserTestUtils.browserLoaded(newTab.linkedBrowser).then(mainPart);
 
   function mainPart() {
     gBrowser.pinTab(newTab);
     gBrowser.selectedTab = newTab;
 
-    openUILinkIn("javascript:var x=0;", "current");
+    openTrustedLinkIn("javascript:var x=0;", "current");
     is(gBrowser.tabs.length, 2, "Should open in current tab");
 
-    openUILinkIn("http://example.com/1", "current");
+    openTrustedLinkIn("http://example.com/1", "current");
     is(gBrowser.tabs.length, 2, "Should open in current tab");
 
-    openUILinkIn("http://example.org/", "current");
+    openTrustedLinkIn("http://example.org/", "current");
     is(gBrowser.tabs.length, 3, "Should open in new tab");
 
     gBrowser.removeTab(newTab);
     gBrowser.removeTab(gBrowser.tabs[1]); // example.org tab
     finish();
   }
   newTab.linkedBrowser.loadURI("http://example.com");
 }
--- a/browser/base/content/test/general/browser_bug594131.js
+++ b/browser/base/content/test/general/browser_bug594131.js
@@ -6,16 +6,18 @@ function test() {
   let newTab = BrowserTestUtils.addTab(gBrowser, "http://example.com");
   waitForExplicitFinish();
   BrowserTestUtils.browserLoaded(newTab.linkedBrowser).then(mainPart);
 
   function mainPart() {
     gBrowser.pinTab(newTab);
     gBrowser.selectedTab = newTab;
 
-    openUILinkIn("http://example.org/", "current", { inBackground: true });
+    openTrustedLinkIn("http://example.org/", "current", {
+      inBackground: true,
+    });
     isnot(gBrowser.selectedTab, newTab, "shouldn't load in background");
 
     gBrowser.removeTab(newTab);
     gBrowser.removeTab(gBrowser.tabs[1]); // example.org tab
     finish();
   }
 }
--- a/browser/base/content/test/general/browser_contentSearchUI.js
+++ b/browser/base/content/test/general/browser_contentSearchUI.js
@@ -711,17 +711,17 @@ async function promiseTab() {
       data: [pageURL],
     });
     promiseMsg("ContentSearch", "AddToWhitelistAck", gMsgMan).then(() => {
       let jsURL = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME;
       gMsgMan.loadFrameScript(jsURL, false);
       deferred.resolve(msg("init"));
     });
   }, true, true);
-  openUILinkIn(pageURL, "current");
+  openTrustedLinkIn(pageURL, "current");
   return deferred.promise;
 }
 
 function promiseMsg(name, type, msgMan) {
   return new Promise(resolve => {
     info("Waiting for " + name + " message " + type + "...");
     msgMan.addMessageListener(name, function onMsg(msgObj) {
       info("Received " + name + " message " + msgObj.data.type + "\n");
--- a/browser/base/content/test/general/browser_remoteWebNavigation_postdata.js
+++ b/browser/base/content/test/general/browser_remoteWebNavigation_postdata.js
@@ -33,17 +33,19 @@ add_task(async function test_remoteWebNa
     let path = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort + "/test";
 
     let postdata =
       "Content-Length: 7\r\n" +
       "Content-Type: application/x-www-form-urlencoded\r\n" +
       "\r\n" +
       "success";
 
-    openUILinkIn(path, "tab", null, makeInputStream(postdata));
-
+    openTrustedLinkIn(path, "tab", {
+       allowThirdPartyFixup: null,
+       postData: makeInputStream(postdata),
+     });
   });
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
   await new Promise(resolve => {
     server.stop(function() { resolve(); });
   });
 });
--- a/browser/base/content/test/tabs/browser_abandonment_telemetry.js
+++ b/browser/base/content/test/tabs/browser_abandonment_telemetry.js
@@ -181,17 +181,17 @@ const PROBE_TESTS = [
   {
     name: "Browsing to a new URL",
 
     category: "newURI",
 
     prepare(browser) {},
 
     async doAction(browser) {
-      openUILinkIn(PAGE_2, "current");
+      openTrustedLinkIn(PAGE_2, "current");
       await BrowserTestUtils.browserLoaded(browser);
     },
   },
 ];
 
 /**
  * Takes a Telemetry histogram snapshot and makes sure
  * that the index for that value (as defined by CATEGORIES)
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -647,21 +647,21 @@ file, You can obtain one at http://mozil
         This is ultimately called by the autocomplete controller as the result
         of handleEnter when the Return key is pressed in the textbox.  Since
         onPopupClick also calls handleEnter, this is also called as a result in
         that case.
 
         @param event
                The event that triggered the command.
         @param openUILinkWhere
-               Optional.  The "where" to pass to openUILinkIn.  This method
+               Optional.  The "where" to pass to openTrustedLinkIn.  This method
                computes the appropriate "where" given the event, but you can
                use this to override it.
         @param openUILinkParams
-               Optional.  The parameters to pass to openUILinkIn.  As with
+               Optional.  The parameters to pass to openTrustedLinkIn.  As with
                "where", this method computes the appropriate parameters, but
                any parameters you supply here will override those.
       -->
       <method name="handleCommand">
         <parameter name="event"/>
         <parameter name="openUILinkWhere"/>
         <parameter name="openUILinkParams"/>
         <parameter name="triggeringPrincipal"/>
@@ -875,17 +875,17 @@ file, You can obtain one at http://mozil
           // area when the current tab is re-selected.
           browser.focus();
 
           if (openUILinkWhere != "current") {
             this.handleRevert();
           }
 
           try {
-            openUILinkIn(url, openUILinkWhere, params);
+            openTrustedLinkIn(url, openUILinkWhere, params);
           } catch (ex) {
             // This load can throw an exception in certain cases, which means
             // we'll want to replace the URL with the loaded URL:
             if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) {
               this.handleRevert();
             }
           }
 
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -115,16 +115,25 @@ function openUILink(url, event, aIgnoreB
       allowThirdPartyFixup: aAllowThirdPartyFixup,
       postData: aPostData,
       referrerURI: aReferrerURI,
       referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_UNSET,
       initiatingDoc: event ? event.target.ownerDocument : null,
     };
   }
 
+  if (!params.triggeringPrincipal) {
+    let dt = event ? event.dataTransfer : null;
+    if (!!dt && dt.mozSourceNode) {
+      params.triggeringPrincipal = dt.mozSourceNode.nodePrincipal;
+    } else {
+      params.triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal({});
+    }
+  }
+
   let where = whereToOpenLink(event, aIgnoreButton, aIgnoreAlt);
   openUILinkIn(url, where, params);
 }
 
 
 /**
  * whereToOpenLink() looks at an event to decide where to open a link.
  *
@@ -176,26 +185,72 @@ function whereToOpenLink(e, ignoreButton
     return "save";
 
   if (shift || (middle && !middleUsesTabs))
     return "window";
 
   return "current";
 }
 
+/* openTrustedLinkIn will attempt to open the given URI using the SystemPrincipal
+ * as the trigeringPrincipal, unless a more specific Principal is provided.
+ *
+ * See openUILinkIn for a discussion of parameters
+ */
+function openTrustedLinkIn(url, where, aParams) {
+  var params = aParams;
+
+  if (!params) {
+    params = {};
+  }
+
+  if (!params.triggeringPrincipal) {
+    params.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+  }
+
+  openUILinkIn(url, where, params);
+}
+
+/* openWebLinkIn will attempt to open the given URI using the NullPrincipal
+ * as the triggeringPrincipal, unless a more specific Principal is provided.
+ *
+ * See openUILinkIn for a discussion of parameters
+ */
+function openWebLinkIn(url, where, params) {
+  if (!params) {
+    params = {};
+  }
+
+  if (!params.triggeringPrincipal) {
+    params.triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal({});
+  }
+
+  openUILinkIn(url, where, params);
+}
+
 /* openUILinkIn opens a URL in a place specified by the parameter |where|.
  *
  * |where| can be:
  *  "current"     current tab            (if there aren't any browser windows, then in a new window instead)
  *  "tab"         new tab                (if there aren't any browser windows, then in a new window instead)
  *  "tabshifted"  same as "tab" but in background if default is to select new tabs, and vice versa
  *  "window"      new window
  *  "save"        save to disk (with no filename hint!)
  *
- * aAllowThirdPartyFixup controls whether third party services such as Google's
+ * DEPRECATION WARNING:
+ * USE        -> openTrustedLinkIn(url, where, aParams) if the source is always
+ *                     a user event on a user- or product-specified URL (as
+ *                     opposed to URLs provided by a webpage)
+ * USE        -> openWebLinkIn(url, where, aParams) if the URI should be loaded
+ *                     with a specific triggeringPrincipal, for instance, if
+ *                     the url was supplied by web content.
+ * DEPRECATED -> openUILinkIn(url, where, AllowThirdPartyFixup, aPostData, ...)
+ *
+ *
+ * allowThirdPartyFixup controls whether third party services such as Google's
  * I Feel Lucky are allowed to interpret this URL. This parameter may be
  * undefined, which is treated as false.
  *
  * Instead of aAllowThirdPartyFixup, you may also pass an object with any of
  * these properties:
  *   allowThirdPartyFixup (boolean)
  *   postData             (nsIInputStream)
  *   referrerURI          (nsIURI)
@@ -207,22 +262,17 @@ function whereToOpenLink(e, ignoreButton
  *   targetBrowser        (XUL browser)
  */
 function openUILinkIn(url, where, aAllowThirdPartyFixup, aPostData, aReferrerURI) {
   var params;
 
   if (arguments.length == 3 && typeof arguments[2] == "object") {
     params = aAllowThirdPartyFixup;
   } else {
-    params = {
-      allowThirdPartyFixup: aAllowThirdPartyFixup,
-      postData: aPostData,
-      referrerURI: aReferrerURI,
-      referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_UNSET,
-    };
+    throw new Error("Required argument triggeringPrincipal missing within openUILinkIn");
   }
 
   params.fromChrome = true;
 
   openLinkIn(url, where, params);
 }
 
 /* eslint-disable complexity */
@@ -824,31 +874,31 @@ function openPreferences(paneID, extraAr
   }
 }
 
 /**
  * Opens the troubleshooting information (about:support) page for this version
  * of the application.
  */
 function openTroubleshootingPage() {
-  openUILinkIn("about:support", "tab");
+  openTrustedLinkIn("about:support", "tab");
 }
 
 /**
  * Opens the feedback page for this version of the application.
  */
 function openFeedbackPage() {
   var url = Services.urlFormatter.formatURLPref("app.feedback.baseURL");
-  openUILinkIn(url, "tab");
+  openTrustedLinkIn(url, "tab");
 }
 
 function openTourPage() {
   let scope = {};
   ChromeUtils.import("resource:///modules/UITour.jsm", scope);
-  openUILinkIn(scope.UITour.url, "tab");
+  openTrustedLinkIn(scope.UITour.url, "tab");
 }
 
 function buildHelpMenu() {
   document.getElementById("feedbackPage")
           .disabled = !Services.policies.isAllowed("feedbackCommands");
 
   document.getElementById("helpSafeMode")
           .disabled = !Services.policies.isAllowed("safeMode");
@@ -915,17 +965,17 @@ function getHelpLinkURL(aHelpTopic) {
 
 // aCalledFromModal is optional
 function openHelpLink(aHelpTopic, aCalledFromModal, aWhere) {
   var url = getHelpLinkURL(aHelpTopic);
   var where = aWhere;
   if (!aWhere)
     where = aCalledFromModal ? "window" : "tab";
 
-  openUILinkIn(url, where);
+  openTrustedLinkIn(url, where);
 }
 
 function openPrefsHelp() {
   // non-instant apply prefwindows are usually modal, so we can't open in the topmost window,
   // since its probably behind the window.
   var instantApply = getBoolPref("browser.preferences.instantApply");
 
   var helpTopic = document.documentElement.getAttribute("helpTopic");
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -739,17 +739,17 @@ if (Services.prefs.getBoolPref("identity
       item.setAttribute("class", "subviewbutton");
       item.setAttribute("targetURI", tabInfo.url);
       item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url);
       item.setAttribute("image", tabInfo.icon);
       item.setAttribute("tooltiptext", tooltipText);
       // We need to use "click" instead of "command" here so openUILink
       // respects different buttons (eg, to open in a new tab).
       item.addEventListener("click", e => {
-        doc.defaultView.openUILink(tabInfo.url, e);
+        doc.defaultView.openWebLinkIn(tabInfo.url, e);
         if (doc.defaultView.whereToOpenLink(e) != "current") {
           e.preventDefault();
           e.stopPropagation();
         } else {
           CustomizableUI.hidePanelForNode(item);
         }
         BrowserUITelemetry.countSyncedTabEvent("open", "toolbarbutton-subview");
       });
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -214,17 +214,17 @@ CustomizeMode.prototype = {
         return;
       }
       let obs = () => {
         Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
         w = this.window.getTopWin(true);
         w.gCustomizeMode.enter();
       };
       Services.obs.addObserver(obs, "browser-delayed-startup-finished");
-      this.window.openUILinkIn("about:newtab", "window");
+      this.window.openTrustedLinkIn("about:newtab", "window");
       return;
     }
     this._wantToBeInCustomizeMode = true;
 
     if (this._customizing || this._handler.isEnteringCustomizeMode) {
       return;
     }
 
@@ -1239,17 +1239,17 @@ CustomizeMode.prototype = {
   openAddonsManagerThemes(aEvent) {
     aEvent.target.parentNode.parentNode.hidePopup();
     this.window.BrowserOpenAddonsMgr("addons://list/theme");
   },
 
   getMoreThemes(aEvent) {
     aEvent.target.parentNode.parentNode.hidePopup();
     let getMoreURL = Services.urlFormatter.formatURLPref("lightweightThemes.getMoreURL");
-    this.window.openUILinkIn(getMoreURL, "tab");
+    this.window.openTrustedLinkIn(getMoreURL, "tab");
   },
 
   updateUIDensity(mode) {
     this.window.gUIDensity.update(mode);
   },
 
   setUIDensity(mode) {
     let win = this.window;
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -560,17 +560,19 @@ const PanelUI = {
     let button = event.target;
     if (event.button > 1 || !button._highlight) {
       return;
     }
     if (event.button == 1) {
       // Bug 1402849, close library panel on mid mouse click
       CustomizableUI.hidePanelForNode(button);
     }
-    window.openUILink(button._highlight.url, event);
+    window.openUILink(button._highlight.url, event, {
+      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})
+    });
   },
 
   /**
    * NB: The enable- and disableSingleSubviewPanelAnimations methods only
    * affect the hiding/showing animations of single-subview panels (tempPanel
    * in the showSubView method).
    */
   disableSingleSubviewPanelAnimations() {
--- a/browser/components/migration/AutoMigrate.jsm
+++ b/browser/components/migration/AutoMigrate.jsm
@@ -671,17 +671,17 @@ const AutoMigrate = {
     let migrationBrowser = this.getBrowserUsedForMigration();
     let rawURL = Preferences.get(kAutoMigrateUndoSurveyPref, "");
     if (!canDoSurveyInLocale || !migrationBrowser || !rawURL) {
       return;
     }
 
     let url = Services.urlFormatter.formatURL(rawURL);
     url = url.replace("%IMPORTEDBROWSER%", encodeURIComponent(migrationBrowser));
-    chromeWindow.openUILinkIn(url, "tab");
+    chromeWindow.openTrustedLinkIn(url, "tab");
   },
 
   QueryInterface: XPCOMUtils.generateQI(
     [Ci.nsIObserver, Ci.nsINavBookmarkObserver, Ci.nsISupportsWeakReference]
   ),
 
   /**
    * Undo action called by the UndoNotification or by the newtab
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -504,17 +504,17 @@ BrowserGlue.prototype = {
           if (win) {
             data = JSON.parse(data);
             let where = win.whereToOpenLink(data);
             // Preserve legacy behavior of non-modifier left-clicks
             // opening in a new selected tab.
             if (where == "current") {
               where = "tab";
             }
-            win.openUILinkIn(data.href, where);
+            win.openTrustedLinkIn(data.href, where);
             linkHandled.data = true;
           }
         }
         break;
       case "profile-before-change":
          // Any component depending on Places should be finalized in
          // _onPlacesShutdown.  Any component that doesn't need to act after
          // the UI has gone should be finalized in _onQuitApplicationGranted.
@@ -834,17 +834,17 @@ BrowserGlue.prototype = {
     let productName = gBrandBundle.GetStringFromName("brandFullName");
     let message = win.gNavigatorBundle.getFormattedString("slowStartup.message", [productName]);
 
     let buttons = [
       {
         label:     win.gNavigatorBundle.getString("slowStartup.helpButton.label"),
         accessKey: win.gNavigatorBundle.getString("slowStartup.helpButton.accesskey"),
         callback() {
-          win.openUILinkIn("https://support.mozilla.org/kb/reset-firefox-easily-fix-most-problems", "tab");
+          win.openTrustedLinkIn("https://support.mozilla.org/kb/reset-firefox-easily-fix-most-problems", "tab");
         }
       },
       {
         label:     win.gNavigatorBundle.getString("slowStartup.disableNotificationButton.label"),
         accessKey: win.gNavigatorBundle.getString("slowStartup.disableNotificationButton.accesskey"),
         callback() {
           Services.prefs.setBoolPref("browser.slowStartup.notificationDisabled", true);
         }
@@ -1480,17 +1480,17 @@ BrowserGlue.prototype = {
       let notifyBox = win.document.getElementById("high-priority-global-notificationbox");
 
       let buttons = [
                       {
                         label,
                         accessKey: key,
                         popup:     null,
                         callback(aNotificationBar, aButton) {
-                          win.openUILinkIn(url, "tab");
+                          win.openTrustedLinkIn(url, "tab");
                         }
                       }
                     ];
 
       notifyBox.appendNotification(text, "post-update-notification",
                                    null, notifyBox.PRIORITY_INFO_LOW,
                                    buttons);
     }
@@ -1507,17 +1507,17 @@ BrowserGlue.prototype = {
     let url = getNotifyString({propName: "alertURL",
                                prefName: "startup.homepage_override_url"});
 
     function clickCallback(subject, topic, data) {
       // This callback will be called twice but only once with this topic
       if (topic != "alertclickcallback")
         return;
       let win = RecentWindow.getMostRecentBrowserWindow();
-      win.openUILinkIn(data, "tab");
+      win.openTrustedLinkIn(data, "tab");
     }
 
     try {
       // This will throw NS_ERROR_NOT_AVAILABLE if the notification cannot
       // be displayed per the idl.
       this.AlertsService.showAlertNotification(null, title, text,
                                           true, url, clickCallback);
     } catch (e) {
@@ -1775,17 +1775,17 @@ BrowserGlue.prototype = {
     var win = RecentWindow.getMostRecentBrowserWindow();
 
     var buttons = [
                     {
                       label:     buttonText,
                       accessKey,
                       popup:     null,
                       callback(aNotificationBar, aButton) {
-                        win.openUILinkIn(url, "tab");
+                        win.openTrustedLinkIn(url, "tab");
                       }
                     }
                   ];
 
     var notifyBox = win.gBrowser.getNotificationBox();
     var notification = notifyBox.appendNotification(text, title, null,
                                                     notifyBox.PRIORITY_CRITICAL_MEDIUM,
                                                     buttons);
@@ -2692,17 +2692,17 @@ BrowserGlue.prototype = {
     }
     let productName = gBrandBundle.GetStringFromName("brandShortName");
     let message = win.gNavigatorBundle.
       getFormattedString("flashHang.message", [productName]);
     let buttons = [{
       label: win.gNavigatorBundle.getString("flashHang.helpButton.label"),
       accessKey: win.gNavigatorBundle.getString("flashHang.helpButton.accesskey"),
       callback() {
-        win.openUILinkIn("https://support.mozilla.org/kb/flash-protected-mode-autodisabled", "tab");
+        win.openTrustedLinkIn("https://support.mozilla.org/kb/flash-protected-mode-autodisabled", "tab");
       }
     }];
     let nb = win.document.getElementById("global-notificationbox");
     nb.appendNotification(message, "flash-hang", null,
                           nb.PRIORITY_INFO_MEDIUM, buttons);
   },
 
   _updateFxaBadges() {
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -748,17 +748,17 @@ var PlacesUIUtils = {
           let browserWin = RecentWindow.getMostRecentBrowserWindow();
           if (browserWin) {
             browserWin.openWebPanel(aNode.title, aNode.uri);
             return;
           }
         }
       }
 
-      aWindow.openUILinkIn(aNode.uri, aWhere, {
+      aWindow.openTrustedLinkIn(aNode.uri, aWhere, {
         allowPopups: aNode.uri.startsWith("javascript:"),
         inBackground: this.loadBookmarksInBackground,
         private: aPrivate,
       });
     }
   },
 
   /**
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -431,17 +431,18 @@ PlacesViewBase.prototype = {
       // Add "Open (Feed Name)" menuitem.
       aPopup._siteURIMenuitem = document.createElement("menuitem");
       aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
       if (typeof this.options.extraClasses.entry == "string") {
         aPopup._siteURIMenuitem.classList.add(this.options.extraClasses.entry);
       }
       aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
       aPopup._siteURIMenuitem.setAttribute("oncommand",
-        "openUILink(this.getAttribute('targetURI'), event);");
+        "openUILink(this.getAttribute('targetURI'), event, {" +
+        " triggeringPrincipal: Services.scriptSecurityManger.createNullPrincipal({})});");
 
       // If a user middle-clicks this item we serve the oncommand event.
       // We are using checkForMiddleClick because of Bug 246720.
       // Note: stopPropagation is needed to avoid serving middle-click
       // with BT_onClick that would open all items in tabs.
       aPopup._siteURIMenuitem.setAttribute("onclick",
         "checkForMiddleClick(this, event); event.stopPropagation();");
       let label =
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -688,17 +688,17 @@ var gMainPane = {
     }
     const win = Services.wm.getMostRecentWindow("navigator:browser");
     if (!win) {
       return;
     }
     const user = await fxAccounts.getSignedInUser();
     if (user) {
       // We have a user, open Sync preferences in the same tab
-      win.openUILinkIn("about:preferences#sync", "current");
+      win.openTrustedLinkIn("about:preferences#sync", "current");
       return;
     }
     let url = await FxAccounts.config.promiseSignInURI("dev-edition-setup");
     let accountsTab = win.gBrowser.addTab(url);
     win.gBrowser.selectedTab = accountsTab;
   },
 
   // HOME PAGE
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -340,17 +340,17 @@ var gSyncPane = {
   _getEntryPoint() {
     let params = new URLSearchParams(document.URL.split("#")[0].split("?")[1] || "");
     return params.get("entrypoint") || "preferences";
   },
 
   openContentInBrowser(url, options) {
     let win = Services.wm.getMostRecentWindow("navigator:browser");
     if (!win) {
-      openUILinkIn(url, "tab");
+      openTrustedLinkIn(url, "tab");
       return;
     }
     win.switchToTabHavingURI(url, true, options);
   },
 
   // Replace the current tab with the specified URL.
   replaceTabWithUrl(url) {
     // Get the <browser> element hosting us.
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -388,17 +388,17 @@
           let params = {
             postData: submission.postData,
           };
           if (aParams) {
             for (let key in aParams) {
               params[key] = aParams[key];
             }
           }
-          openUILinkIn(submission.uri.spec, aWhere, params);
+          openTrustedLinkIn(submission.uri.spec, aWhere, params);
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="command"><![CDATA[
         const target = event.originalTarget;
         if (target.engine) {
--- a/browser/components/search/content/searchReset.js
+++ b/browser/components/search/content/searchReset.js
@@ -47,17 +47,20 @@ function doSearch() {
   window.removeEventListener("unload", recordPageClosed);
 
   let win = window.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIWebNavigation)
                   .QueryInterface(Ci.nsIDocShellTreeItem)
                   .rootTreeItem
                   .QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindow);
-  win.openUILinkIn(submission.uri.spec, "current", false, submission.postData);
+  win.openTrustedLinkIn(submission.uri.spec, "current", {
+    allowThirdPartyFixup: false,
+    postData: submission.postData,
+  });
 }
 
 function openingSettings() {
   record(TELEMETRY_RESULT_ENUM.OPENED_SETTINGS);
   savePref("customized");
   window.removeEventListener("unload", recordPageClosed);
 }
 
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -240,16 +240,17 @@ skip-if = !e10s
 [browser_send_async_message_oom.js]
 [browser_multiple_navigateAndRestore.js]
 run-if = e10s
 [browser_async_window_flushing.js]
 [browser_forget_async_closings.js]
 [browser_newtab_userTypedValue.js]
 [browser_parentProcessRestoreHash.js]
 run-if = e10s
+tags = openUILinkIn
 [browser_sessionStoreContainer.js]
 [browser_windowStateContainer.js]
 skip-if = os == "linux" && !debug
 [browser_1234021.js]
 [browser_remoteness_flip_on_restore.js]
 run-if = e10s
 [browser_background_tab_crash.js]
 run-if = e10s && crashreporter
--- a/browser/components/syncedtabs/TabListComponent.js
+++ b/browser/components/syncedtabs/TabListComponent.js
@@ -108,17 +108,17 @@ TabListComponent.prototype = {
 
   onBookmarkTab(uri, title) {
     this._window.top.PlacesCommandHook
       .bookmarkLink(this._window.top.PlacesUtils.bookmarksMenuFolderId, uri, title)
       .catch(Cu.reportError);
   },
 
   onOpenTab(url, where, params) {
-    this._window.openUILinkIn(url, where, params);
+    this._window.openTrustedLinkIn(url, where, params);
     BrowserUITelemetry.countSyncedTabEvent("open", "sidebar");
   },
 
   onOpenTabs(urls, where) {
     if (!OpenInTabsUtils.confirmOpenInTabs(urls.length, this._window)) {
       return;
     }
     if (where == "window") {
--- a/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js
+++ b/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js
@@ -84,17 +84,17 @@ add_task(async function testActions() {
     top: {
       PlacesCommandHook: {
         bookmarkLink() { return Promise.resolve(); }
       },
       PlacesUtils: { bookmarksMenuFolderId: "id" }
     },
     getBrowserURL() {},
     openDialog() {},
-    openUILinkIn() {}
+    openTrustedLinkIn() {}
   };
   let component = new TabListComponent({
     window: windowMock, store, View: null, SyncedTabs,
     clipboardHelper: clipboardHelperMock,
     getChromeWindow: getChromeWindowMock });
 
   sinon.stub(store, "getData");
   component.onFilter("query");
@@ -128,19 +128,19 @@ add_task(async function testActions() {
   component.onToggleBranch("foo-id");
   Assert.ok(store.toggleBranch.calledWith("foo-id"));
 
   sinon.spy(windowMock.top.PlacesCommandHook, "bookmarkLink");
   component.onBookmarkTab("uri", "title");
   Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][1], "uri");
   Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][2], "title");
 
-  sinon.spy(windowMock, "openUILinkIn");
+  sinon.spy(windowMock, "openTrustedLinkIn");
   component.onOpenTab("uri", "where", "params");
-  Assert.ok(windowMock.openUILinkIn.calledWith("uri", "where", "params"));
+  Assert.ok(windowMock.openTrustedLinkIn.calledWith("uri", "where", "params"));
 
   sinon.spy(chromeWindowMock.gBrowser, "loadTabs");
   let tabsToOpen = ["uri1", "uri2"];
   component.onOpenTabs(tabsToOpen, "where");
   Assert.ok(getChromeWindowMock.calledWith(windowMock));
   Assert.ok(chromeWindowMock.gBrowser.loadTabs.calledWith(tabsToOpen, {
     inBackground: false,
     replace: false,
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -70,17 +70,17 @@ var Translation = {
 
     if (trUI.shouldShowInfoBar(aBrowser.currentURI))
       trUI.showTranslationInfoBar();
   },
 
   openProviderAttribution() {
     let attribution = this.supportedEngines[this.translationEngine];
     ChromeUtils.import("resource:///modules/RecentWindow.jsm");
-    RecentWindow.getMostRecentBrowserWindow().openUILinkIn(attribution, "tab");
+    RecentWindow.getMostRecentBrowserWindow().openTrustedLinkIn(attribution, "tab");
   },
 
   /**
    * The list of translation engines and their attributions.
    */
   supportedEngines: {
     "bing": "http://aka.ms/MicrosoftTranslatorAttribution",
     "yandex": "http://translate.yandex.com/"
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -26,17 +26,17 @@
                    type="arrow" align="start">
           <xul:image class="translation-welcome-logo"/>
           <xul:vbox flex="1" class="translation-welcome-content">
             <xul:description class="translation-welcome-headline"
                              anonid="welcomeHeadline"/>
             <xul:description class="translation-welcome-body" anonid="welcomeBody"/>
             <xul:hbox align="center">
               <xul:label anonid="learnMore" class="plain text-link"
-                         onclick="openUILinkIn('https://support.mozilla.org/kb/automatic-translation', 'tab'); this.parentNode.parentNode.parentNode.hidePopup();"/>
+                         onclick="openTrustedLinkIn('https://support.mozilla.org/kb/automatic-translation', 'tab'); this.parentNode.parentNode.parentNode.hidePopup();"/>
               <xul:spacer flex="1"/>
               <xul:button class="translate-infobar-element" anonid="thanksButton"
                           onclick="this.parentNode.parentNode.parentNode.hidePopup();"/>
             </xul:hbox>
           </xul:vbox>
         </xul:panel>
         <xul:deck anonid="translationStates" selectedIndex="0">
 
--- a/browser/extensions/pocket/content/Pocket.jsm
+++ b/browser/extensions/pocket/content/Pocket.jsm
@@ -20,17 +20,17 @@ var Pocket = {
 
   openList(event) {
     let win = event.view;
     let where = win.whereToOpenLink(event);
     // Never override the current tab unless it's blank:
     if (where == "current" && !win.isTabEmpty(win.gBrowser.selectedTab)) {
       where = "tab";
     }
-    win.openUILinkIn(this.listURL, where);
+    win.openTrustedLinkIn(this.listURL, where);
   },
 
   /**
    * Functions related to the Pocket panel UI.
    */
   onShownInPhotonPageActionPanel(panel, iframe) {
     let window = panel.ownerGlobal;
     window.pktUI.setPhotonPageActionPanelFrame(iframe);
--- a/browser/extensions/pocket/content/main.js
+++ b/browser/extensions/pocket/content/main.js
@@ -144,17 +144,18 @@ var pktUI = (function() {
 
     /**
      * Show the sign-up panel
      */
     function showSignUp() {
         // AB test: Direct logged-out users to tab vs panel
         if (pktApi.getSignupPanelTabTestVariant() == "v2") {
             let site = Services.prefs.getCharPref("extensions.pocket.site");
-            openTabWithUrl("https://" + site + "/firefox_learnmore?s=ffi&t=autoredirect&tv=page_learnmore&src=ff_ext", true);
+            openTabWithUrl("https://" + site + "/firefox_learnmore?s=ffi&t=autoredirect&tv=page_learnmore&src=ff_ext",
+                           Services.scriptSecurityManager.getSystemPrincipal());
 
             // force the panel closed before it opens
             getPanel().hidePopup();
 
             return;
         }
 
         // Control: Show panel as normal
@@ -372,33 +373,21 @@ var pktUI = (function() {
         // send our own "show" event to the panel's script, so the
         // script can prepare the panel for display.
         var _showMessageId = "show";
         pktUIMessaging.addMessageListener(iframe, _showMessageId, function(panelId, data) {
             // Let panel know that it is ready
             pktUIMessaging.sendMessageToPanel(panelId, _showMessageId);
         });
 
-        // Open a new tab with a given url and activate if
+        // Open a new tab with a given url
         var _openTabWithUrlMessageId = "openTabWithUrl";
         pktUIMessaging.addMessageListener(iframe, _openTabWithUrlMessageId, function(panelId, data, contentPrincipal) {
-            try {
-              urlSecurityCheck(data.url, contentPrincipal, Services.scriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
-            } catch (ex) {
-              return;
-            }
-
-            // Check if the tab should become active after opening
-            var activate = true;
-            if (typeof data.activate !== "undefined") {
-                activate = data.activate;
-            }
-
             var url = data.url;
-            openTabWithUrl(url, activate);
+            openTabWithUrl(url, contentPrincipal);
             pktUIMessaging.sendResponseMessageToPanel(panelId, _openTabWithUrlMessageId, url);
         });
 
         // Close the panel
         var _closeMessageId = "close";
         pktUIMessaging.addMessageListener(iframe, _closeMessageId, function(panelId, data) {
             getPanel().hidePopup();
         });
@@ -509,43 +498,49 @@ var pktUI = (function() {
     }
 
     // -- Browser Navigation -- //
 
     /**
      * Open a new tab with a given url and notify the iframe panel that it was opened
      */
 
-    function openTabWithUrl(url) {
+    function openTabWithUrl(url, aTriggeringPrincipal) {
         let recentWindow = Services.wm.getMostRecentWindow("navigator:browser");
         if (!recentWindow) {
           Cu.reportError("Pocket: No open browser windows to openTabWithUrl");
           return;
         }
 
         // If the user is in permanent private browsing than this is not an issue,
         // since the current window will always share the same cookie jar as the other
         // windows.
         if (!PrivateBrowsingUtils.isWindowPrivate(recentWindow) ||
             PrivateBrowsingUtils.permanentPrivateBrowsing) {
-          recentWindow.openUILinkIn(url, "tab");
+          recentWindow.openWebLinkIn(url, "tab", {
+            triggeringPrincipal: aTriggeringPrincipal
+          });
           return;
         }
 
         let windows = Services.wm.getEnumerator("navigator:browser");
         while (windows.hasMoreElements()) {
           let win = windows.getNext();
           if (!PrivateBrowsingUtils.isWindowPrivate(win)) {
-            win.openUILinkIn(url, "tab");
+            win.openWebLinkIn(url, "tab", {
+              triggeringPrincipal: aTriggeringPrincipal
+            });
             return;
           }
         }
 
         // If there were no non-private windows opened already.
-        recentWindow.openUILinkIn(url, "window");
+        recentWindow.openWebLinkIn(url, "window", {
+          triggeringPrincipal: aTriggeringPrincipal
+        });
     }
 
 
     // -- Helper Functions -- //
 
     function getCurrentUrl() {
         return getBrowser().currentURI.spec;
     }
--- a/browser/extensions/screenshots/bootstrap.js
+++ b/browser/extensions/screenshots/bootstrap.js
@@ -104,17 +104,17 @@ const LibraryButton = {
     // this will be null, and we bail out early.
     if (!libraryViewInsertionPoint) {
       return;
     }
     const parent = libraryViewInsertionPoint.parentNode;
     const {nextSibling} = libraryViewInsertionPoint;
     const item = win.document.createElement("toolbarbutton");
     item.className = "subviewbutton subviewbutton-iconic";
-    item.addEventListener("command", () => win.openUILinkIn(this.PAGE_TO_OPEN, "tab"));
+    item.addEventListener("command", () => win.openTrustedLinkIn(this.PAGE_TO_OPEN, "tab"));
     item.id = this.ITEM_ID;
     const iconURL = this.ICON_URL;
     item.setAttribute("image", iconURL);
     item.setAttribute("label", this.LABEL);
 
     parent.insertBefore(item, nextSibling);
   },
 };
--- a/browser/modules/ContentCrashHandlers.jsm
+++ b/browser/modules/ContentCrashHandlers.jsm
@@ -869,17 +869,17 @@ var UnsubmittedCrashHandler = {
         if (onAction) {
           onAction();
         }
       },
     },
     {
       label: gNavigatorBundle.GetStringFromName("pendingCrashReports.viewAll"),
       callback() {
-        chromeWin.openUILinkIn("about:crashes", "tab");
+        chromeWin.openTrustedLinkIn("about:crashes", "tab");
         return true;
       },
     }];
 
     let eventCallback = (eventType) => {
       if (eventType == "dismissed") {
         // The user intentionally dismissed the notification,
         // which we interpret as meaning that they don't care
--- a/browser/modules/ContentSearch.jsm
+++ b/browser/modules/ContentSearch.jsm
@@ -242,17 +242,17 @@ var ContentSearch = {
       browser.loadURI(submission.uri.spec, {
         postData: submission.postData
       });
     } else {
       let params = {
         postData: submission.postData,
         inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
       };
-      win.openUILinkIn(submission.uri.spec, where, params);
+      win.openTrustedLinkIn(submission.uri.spec, where, params);
     }
     win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
                                               { selection: data.selection });
   },
 
   async getSuggestions(engineName, searchString, browser) {
     let engine = Services.search.getEngineByName(engineName);
     if (!engine) {
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -90,17 +90,17 @@ var ExtensionsUI = {
         this.sideloaded.add(addon);
       }
         this._updateNotifications();
     } else {
       // This and all the accompanying about:newaddon code can eventually
       // be removed.  See bug 1331521.
       let win = RecentWindow.getMostRecentBrowserWindow();
       for (let addon of sideloaded) {
-        win.openUILinkIn(`about:newaddon?id=${addon.id}`, "tab");
+        win.openTrustedLinkIn(`about:newaddon?id=${addon.id}`, "tab");
       }
     }
   },
 
   async _checkNewDistroAddons() {
     let newDistroAddons = AddonManagerPrivate.getNewDistroAddons();
     if (!newDistroAddons) {
       return;
--- a/browser/modules/ProcessHangMonitor.jsm
+++ b/browser/modules/ProcessHangMonitor.jsm
@@ -458,17 +458,17 @@ var ProcessHangMonitor = {
                                             [addonName, brandBundle.getString("brandShortName")]);
 
       let linkText = bundle.getString("processHang.add-on.learn-more.text");
       let linkURL = "https://support.mozilla.org/kb/warning-unresponsive-script#w_other-causes";
 
       let link = doc.createElement("label");
       link.setAttribute("class", "text-link");
       link.setAttribute("role", "link");
-      link.setAttribute("onclick", `openUILinkIn(${JSON.stringify(linkURL)}, "tab")`);
+      link.setAttribute("onclick", `openTrustedLinkIn(${JSON.stringify(linkURL)}, "tab")`);
       link.setAttribute("value", linkText);
 
       message = doc.createDocumentFragment();
       message.appendChild(doc.createTextNode(label + " "));
       message.appendChild(link);
 
       buttons.unshift({
         label: bundle.getString("processHang.button_stop_sandbox.label"),
--- a/browser/modules/test/browser/browser.ini
+++ b/browser/modules/test/browser/browser.ini
@@ -2,16 +2,17 @@
 support-files =
   head.js
 
 [browser_BrowserErrorReporter.js]
 support-files =
   browser_BrowserErrorReporter.html
 [browser_BrowserUITelemetry_buckets.js]
 skip-if = !e10s # Bug 1373549
+tags = openUILinkIn
 [browser_BrowserUITelemetry_defaults.js]
 skip-if = !e10s # Bug 1373549
 [browser_BrowserUITelemetry_sidebar.js]
 skip-if = !e10s # Bug 1373549
 [browser_BrowserUITelemetry_syncedtabs.js]
 skip-if = !e10s # Bug 1373549
 [browser_ContentSearch.js]
 support-files =
--- a/devtools/client/aboutdebugging/test/browser_page_not_found.js
+++ b/devtools/client/aboutdebugging/test/browser_page_not_found.js
@@ -15,17 +15,17 @@ add_task(async function() {
   document.querySelector("[aria-controls='addons-panel']").click();
   await waitUntilElement("#addons-panel", document);
 
   await waitForInitialAddonList(document);
   element = document.querySelector(".header-name");
   is(element.textContent, "Add-ons", "Show Addons");
 
   info("Opening about:debugging#invalid-hash");
-  window.openUILinkIn("about:debugging#invalid-hash", "current");
+  window.openTrustedLinkIn("about:debugging#invalid-hash", "current");
   await waitUntilElement(".error-page", document);
 
   element = document.querySelector(".header-name");
   is(element.textContent, "Page not found", "Show error page");
 
   gBrowser.goBack();
   await waitUntilElement("#addons-panel", document);
   await waitForInitialAddonList(document);
--- a/devtools/client/debugger/content/views/sources-view.js
+++ b/devtools/client/debugger/content/views/sources-view.js
@@ -895,17 +895,20 @@ SourcesView.prototype = extend(WidgetMet
   },
 
   /**
    * Opens selected item source in a new tab.
    */
   _onNewTabCommand: function () {
     let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
     let selected = this.selectedItem.attachment;
-    win.openUILinkIn(selected.source.url, "tab", { relatedToCurrent: true });
+    win.openWebLinkIn(selected.source.url, "tab", {
+      triggeringPrincipal: win.document.nodePrincipal,
+      relatedToCurrent: true,
+    });
   },
 
   /**
    * Function called each time a breakpoint item is removed.
    *
    * @param object aItem
    *        The corresponding item.
    */
--- a/devtools/client/debugger/new/panel.js
+++ b/devtools/client/debugger/new/panel.js
@@ -69,21 +69,23 @@ DebuggerPanel.prototype = {
     }
 
     const win = parentDoc.querySelector("window");
     if (!win) {
       return;
     }
 
     const top = win.ownerDocument.defaultView.top;
-    if (!top || typeof top.openUILinkIn !== "function") {
+    if (!top || typeof top.openWebLink !== "function") {
       return;
     }
 
-    top.openUILinkIn(url, "tab");
+    top.openWebLinkIn(url, "tab", {
+      triggeringPrincipal: win.document.nodePrincipal
+    });
   },
 
   openWorkerToolbox: function(worker) {
     this.toolbox.target.client.attachWorker(
       worker.actor,
       (response, workerClient) => {
         const workerTarget = TargetFactory.forWorker(workerClient);
         gDevTools
--- a/devtools/client/dom/dom-panel.js
+++ b/devtools/client/dom/dom-panel.js
@@ -177,17 +177,17 @@ DomPanel.prototype = {
 
     return deferred.promise;
   },
 
   openLink: function(url) {
     let parentDoc = this._toolbox.doc;
     let iframe = parentDoc.getElementById("this._toolbox");
     let top = iframe.ownerDocument.defaultView.top;
-    top.openUILinkIn(url, "tab");
+    top.openWebLinkIn(url, "tab");
   },
 
   getRootGrip: function() {
     let deferred = defer();
 
     // Attach Console. It might involve RDP communication, so wait
     // asynchronously for the result
     this.target.activeConsole.evaluateJSAsync("window", res => {
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -677,17 +677,17 @@ CssComputedView.prototype = {
 
   _onClick: function(event) {
     let target = event.target;
 
     if (target.nodeName === "a") {
       event.stopPropagation();
       event.preventDefault();
       let browserWin = this.inspector.target.tab.ownerDocument.defaultView;
-      browserWin.openUILinkIn(target.href, "tab");
+      browserWin.openWebLinkIn(target.href, "tab");
     }
   },
 
   /**
    * Callback for copy event. Copy selected text.
    *
    * @param {Event} event
    *        copy event object.
@@ -1162,17 +1162,17 @@ PropertyView.prototype = {
   /**
    * The action when a user clicks on the MDN help link for a property.
    */
   mdnLinkClick: function(event) {
     let inspector = this.tree.inspector;
 
     if (inspector.target.tab) {
       let browserWin = inspector.target.tab.ownerDocument.defaultView;
-      browserWin.openUILinkIn(this.link, "tab");
+      browserWin.openWebLinkIn(this.link, "tab");
     }
   },
 
   /**
    * Destroy this property view, removing event listeners
    */
   destroy: function() {
     if (this._matchedSelectorViews) {
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -1122,18 +1122,17 @@ Inspector.prototype = {
       return;
     }
 
     let onExpand = this.markup.expandNode(this.selection.nodeFront);
 
     // Restore the highlighter states prior to emitting "new-root".
     await Promise.all([
       this.highlighters.restoreFlexboxState(),
-      this.highlighters.restoreGridState(),
-      this.highlighters.restoreShapeState()
+      this.highlighters.restoreGridState()
     ]);
 
     this.emit("new-root");
 
     // Wait for full expand of the selected node in order to ensure
     // the markup view is fully emitted before firing 'reloaded'.
     // 'reloaded' is used to know when the panel is fully updated
     // after a page reload.
@@ -1356,17 +1355,17 @@ Inspector.prototype = {
 
     this.teardownToolbar();
     this.breadcrumbs.destroy();
     this.selection.off("new-node-front", this.onNewSelection);
     this.selection.off("detached-front", this.onDetached);
 
     let markupDestroyer = this._destroyMarkup();
 
-    this.highlighters.destroy();
+    let highlighterDestroyer = this.highlighters.destroy();
     this.prefsObserver.destroy();
     this.reflowTracker.destroy();
     this.styleChangeTracker.destroy();
     this.search.destroy();
 
     this._toolbox = null;
     this.breadcrumbs = null;
     this.highlighters = null;
@@ -1379,16 +1378,17 @@ Inspector.prototype = {
     this.search = null;
     this.searchBox = null;
     this.show3PaneToggle = null;
     this.sidebar = null;
     this.store = null;
     this.target = null;
 
     this._panelDestroyer = promise.all([
+      highlighterDestroyer,
       cssPropertiesDestroyer,
       markupDestroyer,
       sidebarDestroyer,
       ruleViewSideBarDestroyer
     ]);
 
     return this._panelDestroyer;
   },
@@ -2332,17 +2332,17 @@ Inspector.prototype = {
     }
 
     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") {
             let browserWin = this.target.tab.ownerDocument.defaultView;
-            browserWin.openUILinkIn(url, "tab");
+            browserWin.openWebLinkIn(url, "tab");
           } 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/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -19,16 +19,17 @@ const ClassListPreviewer = require("devt
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 const {
   VIEW_NODE_SELECTOR_TYPE,
   VIEW_NODE_PROPERTY_TYPE,
   VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_IMAGE_URL_TYPE,
   VIEW_NODE_LOCATION_TYPE,
   VIEW_NODE_SHAPE_POINT_TYPE,
+  VIEW_NODE_SHAPE_SWATCH,
   VIEW_NODE_VARIABLE_TYPE,
   VIEW_NODE_FONT_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
 const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
 const {createChild, promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {debounce} = require("devtools/shared/debounce");
 const EventEmitter = require("devtools/shared/event-emitter");
@@ -356,16 +357,23 @@ CssRuleView.prototype = {
         enabled: prop.enabled,
         overridden: prop.overridden,
         pseudoElement: prop.rule.pseudoElement,
         sheetHref: prop.rule.domRule.href,
         textProperty: prop,
         toggleActive: getShapeToggleActive(node),
         point: getShapePoint(node)
       };
+    } else if (classes.contains("ruleview-shapeswatch") && prop) {
+      type = VIEW_NODE_SHAPE_SWATCH;
+      value = {
+        enabled: prop.enabled,
+        overridden: prop.overridden,
+        textProperty: prop,
+      };
     } else if ((classes.contains("ruleview-variable") ||
                 classes.contains("ruleview-unmatched-variable")) && prop) {
       type = VIEW_NODE_VARIABLE_TYPE;
       value = {
         property: getPropertyNameAndValue(node).name,
         value: node.textContent,
         enabled: prop.enabled,
         overridden: prop.overridden,
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_01.js
@@ -20,28 +20,28 @@ const TEST_URI = `
 
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Select a node with a shape value");
   await selectNode("#shape", inspector);
   let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
   let shapesToggle = container.querySelector(".ruleview-shapeswatch");
 
   info("Checking the initial state of the CSS shape toggle in the rule-view.");
   ok(shapesToggle, "Shapes highlighter toggle is visible.");
   ok(!shapesToggle.classList.contains("active"),
     "Shapes highlighter toggle button is not active.");
   ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
     "No CSS shapes highlighter exists in the rule-view.");
   ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown.");
-
   info("Toggling ON the CSS shapes highlighter from the rule-view.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapesToggle.click();
   await onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created and toggle button is active in " +
     "the rule-view.");
   ok(shapesToggle.classList.contains("active"),
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_03.js
@@ -59,17 +59,18 @@ add_task(async function() {
   container = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
   shapeToggle = container.querySelector(".ruleview-shapeswatch");
 
   info("Checking the state of the CSS shapes toggle for the second shapes container " +
     "in the rule-view.");
   ok(shapeToggle, "shapes highlighter toggle is visible.");
   ok(!shapeToggle.classList.contains("active"),
     "shapes highlighter toggle button is not active.");
-  ok(highlighters.shapesHighlighterShown, "CSS shapes highlighter is still shown.");
+  ok(!highlighters.shapesHighlighterShown, "CSS shapes highlighter is still no longer" +
+    "shown due to selecting another node.");
 
   info("Toggling ON the CSS shapes highlighter for the second shapes container " +
     "from the rule-view.");
   onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapeToggle.click();
   await onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created for the second shapes container " +
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_04.js
@@ -18,16 +18,17 @@ const TEST_URI = `
   <div id="shape"></div>
 `;
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Select a node with a shape value");
   await selectNode("#shape", inspector);
   let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
   let shapeToggle = container.querySelector(".ruleview-shapeswatch");
 
   info("Toggling ON the CSS shape highlighter from the rule-view.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapeToggle.click();
   await onHighlighterShown;
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_05.js
@@ -18,16 +18,17 @@ const TEST_URI = `
   <div id="shape"></div>
 `;
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view, testActor} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Select a node with a shape value");
   await selectNode("#shape", inspector);
   let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
   let shapeToggle = container.querySelector(".ruleview-shapeswatch");
 
   info("Toggling ON the CSS shapes highlighter from the rule-view.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   shapeToggle.click();
   await onHighlighterShown;
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_06.js
@@ -20,16 +20,17 @@ const TEST_URI = `
   <div class="shape" id="shape2"></div>
 `;
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Selecting the first shapes container.");
   await selectNode("#shape1", inspector);
   let clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
   let clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shapeswatch");
   let shapeOutsideContainer = getRuleViewProperty(view, ".shape",
     "shape-outside").valueSpan;
   let shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shapeswatch");
 
   info("Toggling ON the CSS shapes highlighter for clip-path from the rule-view.");
@@ -58,21 +59,9 @@ add_task(async function() {
   clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shapeswatch");
   shapeOutsideContainer = getRuleViewProperty(view, ".shape",
     "shape-outside").valueSpan;
   shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shapeswatch");
   ok(!clipPathShapeToggle.classList.contains("active"),
      "clip-path toggle button is not active.");
   ok(!shapeOutsideToggle.classList.contains("active"),
      "shape-outside toggle button is not active.");
-
-  info("Selecting the first shapes container.");
-  await selectNode("#shape1", inspector);
-  clipPathContainer = getRuleViewProperty(view, ".shape", "clip-path").valueSpan;
-  clipPathShapeToggle = clipPathContainer.querySelector(".ruleview-shapeswatch");
-  shapeOutsideContainer = getRuleViewProperty(view, ".shape",
-    "shape-outside").valueSpan;
-  shapeOutsideToggle = shapeOutsideContainer.querySelector(".ruleview-shapeswatch");
-  ok(!clipPathShapeToggle.classList.contains("active"),
-     "clip-path toggle button is not active.");
-  ok(shapeOutsideToggle.classList.contains("active"),
-     "shape-outside toggle button is active.");
 });
--- a/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js
+++ b/devtools/client/inspector/rules/test/browser_rules_shapes-toggle_07.js
@@ -19,25 +19,21 @@ const TEST_URI = `
 
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
   await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
   let {inspector, view} = await openRuleView();
   let highlighters = view.highlighters;
 
+  info("Select a node with a shape value");
   await selectNode("#shape", inspector);
   let container = getRuleViewProperty(view, "#shape", "clip-path").valueSpan;
   let shapesToggle = container.querySelector(".ruleview-shapeswatch");
 
-  info("Checking the initial state of the CSS shape toggle in the rule-view.");
-  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
-    "No CSS shapes highlighter exists in the rule-view.");
-  ok(!highlighters.shapesHighlighterShown, "No CSS shapes highlighter is shown.");
-
   info("Toggling ON the CSS shapes highlighter with transform mode on.");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   EventUtils.sendMouseEvent({type: "click", metaKey: true, ctrlKey: true},
     shapesToggle, view.styleWindow);
   await onHighlighterShown;
 
   info("Checking the CSS shapes highlighter is created and transform mode is on");
   ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -22,17 +22,16 @@ const Services = require("Services");
 
 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";
-const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
 const FONT_FAMILY_CLASS = "ruleview-font-family";
 const SHAPE_SWATCH_CLASS = "ruleview-shapeswatch";
 
 /*
  * An actionable element is an element which on click triggers a specific action
  * (e.g. shows a color tooltip, opens a link, …).
  */
 const ACTIONABLE_ELEMENTS_SELECTORS = [
@@ -91,17 +90,16 @@ function TextPropertyEditor(ruleEditor, 
   this._onNameDone = this._onNameDone.bind(this);
   this._onValueDone = this._onValueDone.bind(this);
   this._onSwatchCommit = this._onSwatchCommit.bind(this);
   this._onSwatchPreview = this._onSwatchPreview.bind(this);
   this._onSwatchRevert = this._onSwatchRevert.bind(this);
   this._onValidate = this.ruleView.debounce(this._previewValue, 10, this);
   this.update = this.update.bind(this);
   this.updatePropertyState = this.updatePropertyState.bind(this);
-  this._onHoverShapePoint = this._onHoverShapePoint.bind(this);
 
   this._create();
   this.update();
 }
 
 TextPropertyEditor.prototype = {
   /**
    * Boolean indicating if the name or value is being currently edited.
@@ -294,17 +292,17 @@ TextPropertyEditor.prototype = {
 
       this.valueSpan.addEventListener("click", (event) => {
         let target = event.target;
 
         if (target.nodeName === "a") {
           event.stopPropagation();
           event.preventDefault();
           let browserWin = this.ruleView.inspector.target.tab.ownerDocument.defaultView;
-          browserWin.openUILinkIn(target.href, "tab");
+          browserWin.openTrustedLinkIn(target.href, "tab");
         }
       });
 
       editableField({
         start: this._onStartEditing,
         element: this.valueSpan,
         done: this._onValueDone,
         destroy: this.update,
@@ -314,18 +312,16 @@ TextPropertyEditor.prototype = {
         property: this.prop,
         defaultIncrement: this.prop.name === "opacity" ? 0.1 : 1,
         popup: this.popup,
         multiline: true,
         maxWidth: () => this.container.getBoundingClientRect().width,
         cssProperties: this.cssProperties,
         cssVariables: this.rule.elementStyle.variables,
       });
-
-      this.ruleView.highlighters.on("hover-shape-point", this._onHoverShapePoint);
     }
   },
 
   /**
    * Get the path from which to resolve requests for this
    * rule's stylesheet.
    *
    * @return {String} the stylesheet's href.
@@ -510,23 +506,16 @@ TextPropertyEditor.prototype = {
     }
 
     let shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch");
     if (shapeToggle) {
       let mode = "css" + name.split("-").map(s => {
         return s[0].toUpperCase() + s.slice(1);
       }).join("");
       shapeToggle.setAttribute("data-mode", mode);
-
-      let { highlighters, inspector } = this.ruleView;
-      if (highlighters.shapesHighlighterShown === inspector.selection.nodeFront &&
-          highlighters.state.shapes.options.mode === mode) {
-        shapeToggle.classList.add("active");
-        highlighters.highlightRuleViewShapePoint(highlighters.state.shapes.hoverPoint);
-      }
     }
 
     // Now that we have updated the property's value, we might have a pending
     // click on the value container. If we do, we have to trigger a click event
     // on the right element.
     if (this._hasPendingClick) {
       this._hasPendingClick = false;
       let elToClick;
@@ -1040,72 +1029,11 @@ TextPropertyEditor.prototype = {
    * Returns true if the property is a `display: [inline-]grid` declaration.
    *
    * @return {Boolean} true if the property is a `display: [inline-]grid` declaration.
    */
   isDisplayGrid: function() {
     return this.prop.name === "display" &&
       (this.prop.value === "grid" || this.prop.value === "inline-grid");
   },
-
-  /**
-   * Highlight the given shape point in the rule view. Called when "hover-shape-point"
-   * event is emitted.
-   *
-   * @param {Event} event
-   *        The "hover-shape-point" event.
-   * @param {String} point
-   *        The point to highlight.
-   */
-  _onHoverShapePoint: function(point) {
-    // If there is no shape toggle, or it is not active, return.
-    let shapeToggle = this.valueSpan.querySelector(".ruleview-shapeswatch.active");
-    if (!shapeToggle) {
-      return;
-    }
-
-    let view = this.ruleView;
-    let { highlighters } = view;
-    let ruleViewEl = view.element;
-    let selector = `.ruleview-shape-point.active`;
-    for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
-      this._toggleShapePointActive(pointNode, false);
-    }
-
-    if (typeof point === "string") {
-      if (point.includes(",")) {
-        point = point.split(",")[0];
-      }
-      // Because one inset value can represent multiple points, inset points use classes
-      // instead of data.
-      selector = (INSET_POINT_TYPES.includes(point)) ?
-                 `.ruleview-shape-point.${point}` :
-                 `.ruleview-shape-point[data-point='${point}']`;
-      for (let pointNode of this.valueSpan.querySelectorAll(selector)) {
-        let nodeInfo = view.getNodeInfo(pointNode);
-        if (highlighters.isRuleViewShapePoint(nodeInfo)) {
-          this._toggleShapePointActive(pointNode, true);
-        }
-      }
-    }
-  },
-
-  /**
-   * Toggle the class "active" on the given shape point in the rule view if the current
-   * inspector selection is highlighted by the shapes highlighter.
-   *
-   * @param {NodeFront} node
-   *        The NodeFront of the shape point to toggle
-   * @param {Boolean} active
-   *        Whether the shape point should be active
-   */
-  _toggleShapePointActive: function(node, active) {
-    let { highlighters } = this.ruleView;
-    if (highlighters.inspector.selection.nodeFront !=
-        highlighters.shapesHighlighterShown) {
-      return;
-    }
-
-    node.classList.toggle("active", active);
-  },
 };
 
 module.exports = TextPropertyEditor;
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -7,31 +7,39 @@
 "use strict";
 
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {
   VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_SHAPE_POINT_TYPE
 } = require("devtools/client/inspector/shared/node-types");
-
 const DEFAULT_GRID_COLOR = "#4B0082";
-const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
 
 /**
  * Highlighters overlay is a singleton managing all highlighters in the Inspector.
  */
 class HighlightersOverlay {
   /**
    * @param  {Inspector} inspector
    *         Inspector toolbox panel.
    */
   constructor(inspector) {
+    /*
+    * Collection of instantiated highlighter actors like FlexboxHighlighter,
+    * CssGridHighlighter, ShapesHighlighter and GeometryEditorHighlighter.
+    */
+    this.highlighters = {};
+    /*
+    * Collection of instantiated in-context editors, like ShapesInContextEditor, which
+    * behave like highlighters but with added editing capabilities that need to map value
+    * changes to properties in the Rule view.
+    */
+    this.editors = {};
     this.inspector = inspector;
-    this.highlighters = {};
     this.highlighterUtils = this.inspector.toolbox.highlighterUtils;
 
     // Only initialize the overlay if at least one of the highlighter types is supported.
     this.supportsHighlighters = this.highlighterUtils.supportsCustomHighlighters();
 
     // NodeFront of the flexbox container that is highlighted.
     this.flexboxHighlighterShown = null;
     // NodeFront of element that is highlighted by the geometry editor.
@@ -58,17 +66,18 @@ class HighlightersOverlay {
     this.onWillNavigate = this.onWillNavigate.bind(this);
     this.hideFlexboxHighlighter = this.hideFlexboxHighlighter.bind(this);
     this.hideGridHighlighter = this.hideGridHighlighter.bind(this);
     this.hideShapesHighlighter = this.hideShapesHighlighter.bind(this);
     this.showFlexboxHighlighter = this.showFlexboxHighlighter.bind(this);
     this.showGridHighlighter = this.showGridHighlighter.bind(this);
     this.showShapesHighlighter = this.showShapesHighlighter.bind(this);
     this._handleRejection = this._handleRejection.bind(this);
-    this._onHighlighterEvent = this._onHighlighterEvent.bind(this);
+    this.onShapesHighlighterShown = this.onShapesHighlighterShown.bind(this);
+    this.onShapesHighlighterHidden = this.onShapesHighlighterHidden.bind(this);
 
     // Add inspector events, not specific to a given view.
     this.inspector.on("markupmutation", this.onMarkupMutation);
     this.inspector.target.on("will-navigate", this.onWillNavigate);
 
     EventEmitter.decorate(this);
   }
 
@@ -116,91 +125,88 @@ class HighlightersOverlay {
 
     let el = view.element;
     el.removeEventListener("click", this.onClick, true);
     el.removeEventListener("mousemove", this.onMouseMove);
     el.removeEventListener("mouseout", this.onMouseOut);
   }
 
   /**
-   * Toggle the shapes highlighter for the given element with a shape.
-   *
+   * Toggle the shapes highlighter for the given node.
+
    * @param  {NodeFront} node
    *         The NodeFront of the element with a shape to highlight.
    * @param  {Object} options
    *         Object used for passing options to the shapes highlighter.
+   * @param {TextProperty} textProperty
+   *        TextProperty where to write changes.
    */
-  async toggleShapesHighlighter(node, options = {}) {
-    options.transformMode = options.ctrlOrMetaPressed;
-
-    if (node == this.shapesHighlighterShown &&
-        options.mode === this.state.shapes.options.mode) {
-      // If meta/ctrl is not pressed, hide the highlighter.
-      if (!options.ctrlOrMetaPressed) {
-        await this.hideShapesHighlighter(node);
-        return;
-      }
-
-      // If meta/ctrl is pressed, toggle transform mode on the highlighter.
-      options.transformMode = !this.state.shapes.options.transformMode;
+  async toggleShapesHighlighter(node, options, textProperty) {
+    let shapesEditor = await this.getInContextEditor("shapesEditor");
+    if (!shapesEditor) {
+      return;
     }
-
-    await this.showShapesHighlighter(node, options);
+    shapesEditor.toggle(node, options, textProperty);
   }
 
   /**
-   * Show the shapes highlighter for the given element with a shape.
+   * Show the shapes highlighter for the given node.
+   * This method delegates to the in-context shapes editor.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the element with a shape to highlight.
    * @param  {Object} options
    *         Object used for passing options to the shapes highlighter.
    */
   async showShapesHighlighter(node, options) {
-    let highlighter = await this._getHighlighter("ShapesHighlighter");
-    if (!highlighter) {
-      return;
-    }
-
-    let isShown = await highlighter.show(node, options);
-    if (!isShown) {
+    let shapesEditor = await this.getInContextEditor("shapesEditor");
+    if (!shapesEditor) {
       return;
     }
-
-    this.shapesHighlighterShown = node;
-    let { mode } = options;
-    this._toggleRuleViewIcon(node, false, ".ruleview-shapeswatch");
-    this._toggleRuleViewIcon(node, true, `.ruleview-shapeswatch[data-mode='${mode}']`);
-
-    try {
-      // Save shapes highlighter state.
-      let { url } = this.inspector.target;
-      let selector = await node.getUniqueSelector();
-      this.state.shapes = { selector, options, url };
-      this.shapesHighlighterShown = node;
-      this.emit("shapes-highlighter-shown", node, options);
-    } catch (e) {
-      this._handleRejection(e);
-    }
+    shapesEditor.show(node, options);
   }
 
   /**
-   * Hide the shapes highlighter for the given element with a shape.
+   * Called after the shape highlighter was shown.
    *
-   * @param  {NodeFront} node
-   *         The NodeFront of the element with a shape to unhighlight.
+   * @param  {Object} data
+   *         Data associated with the event.
+   *         Contains:
+   *         - {NodeFront} node: The NodeFront of the element that is highlighted.
+   *         - {Object} options: Options that were passed to ShapesHighlighter.show()
    */
-  async hideShapesHighlighter(node) {
-    if (!this.shapesHighlighterShown || !this.highlighters.ShapesHighlighter) {
+  onShapesHighlighterShown(data) {
+    let { node, options } = data;
+    this.shapesHighlighterShown = node;
+    this.state.shapes.options = options;
+    this.emit("shapes-highlighter-shown", node, options);
+  }
+
+  /**
+   * Hide the shapes highlighter if visible.
+   * This method delegates the to the in-context shapes editor which wraps
+   * the shapes highlighter with additional functionality.
+   */
+  async hideShapesHighlighter() {
+    let shapesEditor = await this.getInContextEditor("shapesEditor");
+    if (!shapesEditor) {
       return;
     }
+    shapesEditor.hide();
+  }
 
-    this._toggleRuleViewIcon(node, false, ".ruleview-shapeswatch");
-
-    await this.highlighters.ShapesHighlighter.hide();
+  /**
+   * Called after the shapes highlighter was hidden.
+   *
+   * @param  {Object} data
+   *         Data associated with the event.
+   *         Contains:
+   *         - {NodeFront} node: The NodeFront of the element that was highlighted.
+   */
+  onShapesHighlighterHidden(data) {
     this.emit("shapes-highlighter-hidden", this.shapesHighlighterShown,
       this.state.shapes.options);
     this.shapesHighlighterShown = null;
     this.state.shapes = {};
   }
 
   /**
    * Show the shapes highlighter for the given element, with the given point highlighted.
@@ -214,47 +220,16 @@ class HighlightersOverlay {
     if (node == this.shapesHighlighterShown) {
       let options = Object.assign({}, this.state.shapes.options);
       options.hoverPoint = point;
       await this.showShapesHighlighter(node, options);
     }
   }
 
   /**
-   * Highlight the given shape point in the rule view.
-   *
-   * @param {String} point
-   *        The point to highlight.
-   */
-  highlightRuleViewShapePoint(point) {
-    let view = this.inspector.getPanel("ruleview").view;
-    let ruleViewEl = view.element;
-    let selector = `.ruleview-shape-point.active`;
-
-    for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
-      this._toggleShapePointActive(pointNode, false);
-    }
-
-    if (point !== null && point !== undefined) {
-      // Because one inset value can represent multiple points, inset points use classes
-      // instead of data.
-      selector = (INSET_POINT_TYPES.includes(point)) ?
-                 `.ruleview-shape-point.${point}` :
-                 `.ruleview-shape-point[data-point='${point}']`;
-
-      for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
-        let nodeInfo = view.getNodeInfo(pointNode);
-        if (this.isRuleViewShapePoint(nodeInfo)) {
-          this._toggleShapePointActive(pointNode, true);
-        }
-      }
-    }
-  }
-
-  /**
    * Toggle the flexbox highlighter for the given flexbox container element.
    *
    * @param  {NodeFront} node
    *         The NodeFront of the flexbox container element to highlight.
    * @param  {Object} options
    *         Object used for passing options to the flexbox highlighter.
    */
   async toggleFlexboxHighlighter(node, options = {}) {
@@ -460,34 +435,16 @@ class HighlightersOverlay {
 
     await this.highlighters.GeometryEditorHighlighter.hide();
 
     this.emit("geometry-editor-highlighter-hidden");
     this.geometryEditorHighlighterShown = null;
   }
 
   /**
-   * Handle events emitted by the highlighter.
-   *
-   * @param {Object} data
-   *        The data object sent in the event.
-   */
-  _onHighlighterEvent(data) {
-    if (data.type === "shape-hover-on") {
-      this.state.shapes.hoverPoint = data.point;
-      this.emit("hover-shape-point", data.point);
-    } else if (data.type === "shape-hover-off") {
-      this.state.shapes.hoverPoint = null;
-      this.emit("hover-shape-point", null);
-    }
-
-    this.emit("highlighter-event-handled");
-  }
-
-  /**
    * Restores the saved flexbox highlighter state.
    */
   async restoreFlexboxState() {
     try {
       await this.restoreState("flexbox", this.state.flexbox, this.showFlexboxHighlighter);
     } catch (e) {
       this._handleRejection(e);
     }
@@ -500,29 +457,18 @@ class HighlightersOverlay {
     try {
       await this.restoreState("grid", this.state.grid, this.showGridHighlighter);
     } catch (e) {
       this._handleRejection(e);
     }
   }
 
   /**
-   * Restores the saved shape highlighter state.
-   */
-  async restoreShapeState() {
-    try {
-      await this.restoreState("shapes", this.state.shapes, this.showShapesHighlighter);
-    } catch (e) {
-      this._handleRejection(e);
-    }
-  }
-
-  /**
-   * Helper function called by restoreFlexboxState, restoreGridState and
-   * restoreShapeState. Restores the saved highlighter state for the given highlighter
+   * Helper function called by restoreFlexboxState, restoreGridState.
+   * Restores the saved highlighter state for the given highlighter
    * and their state.
    *
    * @param  {String} name
    *         The name of the highlighter to be restored
    * @param  {Object} state
    *         The state of the highlighter to be restored
    * @param  {Function} showFunction
    *         The function that shows the highlighter
@@ -550,16 +496,57 @@ class HighlightersOverlay {
       await showFunction(nodeFront, options);
       this.emit(`${name}-state-restored`, { restored: true });
     }
 
     this.emit(`${name}-state-restored`, { restored: false });
   }
 
   /**
+  * Get an instance of an in-context editor for the given type.
+  *
+  * In-context editors behave like highlighters but with added editing capabilities which
+  * need to write value changes back to something, like to properties in the Rule view.
+  * They typically exist in the context of the page, like the ShapesInContextEditor.
+  *
+  * @param  {String} type
+  *         Type of in-context editor. Currently supported: "shapesEditor"
+  *
+  * @return {Object|null}
+  *         Reference to instance for given type of in-context editor or null.
+  */
+  async getInContextEditor(type) {
+    if (this.editors[type]) {
+      return this.editors[type];
+    }
+
+    let editor;
+
+    switch (type) {
+      case "shapesEditor":
+        let highlighter = await this._getHighlighter("ShapesHighlighter");
+        if (!highlighter) {
+          return null;
+        }
+        const ShapesInContextEditor = require("devtools/client/shared/widgets/ShapesInContextEditor");
+
+        editor = new ShapesInContextEditor(highlighter, this.inspector, this.state);
+        editor.on("show", this.onShapesHighlighterShown);
+        editor.on("hide", this.onShapesHighlighterHidden);
+        break;
+      default:
+        throw new Error(`Unsupported in-context editor '${name}'`);
+    }
+
+    this.editors[type] = editor;
+
+    return editor;
+  }
+
+  /**
    * Get a highlighter front given a type. It will only be initialized once.
    *
    * @param  {String} type
    *         The highlighter type. One of this.highlighters.
    * @return {Promise} that resolves to the highlighter
    */
   async _getHighlighter(type) {
     let utils = this.highlighterUtils;
@@ -575,17 +562,16 @@ class HighlightersOverlay {
     } catch (e) {
       // Ignore any error
     }
 
     if (!highlighter) {
       return null;
     }
 
-    highlighter.on("highlighter-event", this._onHighlighterEvent);
     this.highlighters[type] = highlighter;
     return highlighter;
   }
 
   _handleRejection(error) {
     if (!this.destroyed) {
       console.error(error);
     }
@@ -717,17 +703,17 @@ class HighlightersOverlay {
 
   /**
    * Does the current clicked node have the shapes highlighter toggle in the
    * rule-view.
    *
    * @param  {DOMNode} node
    * @return {Boolean}
    */
-  _isRuleViewShape(node) {
+  _isRuleViewShapeSwatch(node) {
     return this.isRuleView(node) && node.classList.contains("ruleview-shapeswatch");
   }
 
   /**
    * Is the current hovered node a css transform property value in the rule-view.
    *
    * @param  {Object} nodeInfo
    * @return {Boolean}
@@ -771,28 +757,34 @@ class HighlightersOverlay {
       let { store } = this.inspector;
       let { grids, highlighterSettings } = store.getState();
       let grid = grids.find(g => g.nodeFront == this.inspector.selection.nodeFront);
 
       highlighterSettings.color = grid ? grid.color : DEFAULT_GRID_COLOR;
 
       this.toggleGridHighlighter(this.inspector.selection.nodeFront, highlighterSettings,
         "rule");
-    } else if (this._isRuleViewDisplayFlex(event.target)) {
+    }
+
+    if (this._isRuleViewDisplayFlex(event.target)) {
       event.stopPropagation();
 
       this.toggleFlexboxHighlighter(this.inspector.selection.nodeFront);
-    } else if (this._isRuleViewShape(event.target)) {
+    }
+
+    if (this._isRuleViewShapeSwatch(event.target)) {
       event.stopPropagation();
 
-      let settings = {
+      const view = this.inspector.getPanel("ruleview").view;
+      const nodeInfo = view.getNodeInfo(event.target);
+
+      this.toggleShapesHighlighter(this.inspector.selection.nodeFront, {
         mode: event.target.dataset.mode,
-        ctrlOrMetaPressed: event.metaKey || event.ctrlKey
-      };
-      this.toggleShapesHighlighter(this.inspector.selection.nodeFront, settings);
+        transformMode: event.metaKey || event.ctrlKey
+      }, nodeInfo.value.textProperty);
     }
   }
 
   onMouseMove(event) {
     // Bail out if the target is the same as for the last mousemove.
     if (event.target === this._lastHovered) {
       return;
     }
@@ -808,17 +800,16 @@ class HighlightersOverlay {
     let nodeInfo = view.getNodeInfo(event.target);
     if (!nodeInfo) {
       return;
     }
 
     if (this.isRuleViewShapePoint(nodeInfo)) {
       let { point } = nodeInfo.value;
       this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, point);
-      this.emit("hover-shape-point", point);
       return;
     }
 
     // Choose the type of highlighter required for the hovered node.
     let type;
     if (this._isRuleViewTransform(nodeInfo) ||
         this._isComputedViewTransform(nodeInfo)) {
       type = "CssTransformHighlighter";
@@ -846,17 +837,16 @@ class HighlightersOverlay {
 
     // Otherwise, hide the highlighter.
     let view = this.isRuleView(this._lastHovered) ?
       this.inspector.getPanel("ruleview").view :
       this.inspector.getPanel("computedview").computedView;
     let nodeInfo = view.getNodeInfo(this._lastHovered);
     if (nodeInfo && this.isRuleViewShapePoint(nodeInfo)) {
       this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, null);
-      this.emit("hover-shape-point", null);
     }
     this._lastHovered = null;
     this._hideHoveredHighlighter();
   }
 
   /**
    * Handler function for "markupmutation" events. Hides the flexbox/grid/shapes
    * highlighter if the flexbox/grid/shapes container is no longer in the DOM tree.
@@ -882,41 +872,61 @@ class HighlightersOverlay {
    */
   onWillNavigate() {
     this.flexboxHighlighterShown = null;
     this.geometryEditorHighlighterShown = null;
     this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
     this.selectorHighlighterShown = null;
     this.shapesHighlighterShown = null;
+    this.destroyEditors();
+  }
+
+  /**
+  * Destroy and clean-up all instances of in-context editors.
+  */
+  destroyEditors() {
+    for (let type in this.editors) {
+      this.editors[type].off("show");
+      this.editors[type].off("hide");
+      this.editors[type].destroy();
+    }
+
+    this.editors = {};
+  }
+
+  /**
+  * Destroy and clean-up all instances of highlighters.
+  */
+  destroyHighlighters() {
+    for (let type in this.highlighters) {
+      if (this.highlighters[type]) {
+        this.highlighters[type].finalize();
+        this.highlighters[type] = null;
+      }
+    }
+
+    this.highlighters = null;
   }
 
   /**
    * Destroy this overlay instance, removing it from the view and destroying
    * all initialized highlighters.
    */
-  destroy() {
-    for (let type in this.highlighters) {
-      if (this.highlighters[type]) {
-        if (this.highlighters[type].off) {
-          this.highlighters[type].off("highlighter-event", this._onHighlighterEvent);
-        }
-        this.highlighters[type].finalize();
-        this.highlighters[type] = null;
-      }
-    }
+  async destroy() {
+    this.destroyHighlighters();
+    this.destroyEditors();
 
     // Remove inspector events.
     this.inspector.off("markupmutation", this.onMarkupMutation);
     this.inspector.target.off("will-navigate", this.onWillNavigate);
 
     this._lastHovered = null;
 
     this.inspector = null;
-    this.highlighters = null;
     this.highlighterUtils = null;
     this.supportsHighlighters = null;
     this.state = null;
 
     this.flexboxHighlighterShown = null;
     this.geometryEditorHighlighterShown = null;
     this.gridHighlighterShown = null;
     this.hoveredHighlighterShown = null;
--- a/devtools/client/inspector/shared/node-types.js
+++ b/devtools/client/inspector/shared/node-types.js
@@ -13,8 +13,9 @@
 exports.VIEW_NODE_SELECTOR_TYPE = 1;
 exports.VIEW_NODE_PROPERTY_TYPE = 2;
 exports.VIEW_NODE_VALUE_TYPE = 3;
 exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
 exports.VIEW_NODE_LOCATION_TYPE = 5;
 exports.VIEW_NODE_SHAPE_POINT_TYPE = 6;
 exports.VIEW_NODE_VARIABLE_TYPE = 7;
 exports.VIEW_NODE_FONT_TYPE = 8;
+exports.VIEW_NODE_SHAPE_SWATCH = 9;
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_04.js
@@ -5,233 +5,320 @@
 "use strict";
 
 // Test that shapes are updated correctly on mouse events.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
-  let inspector = await openInspectorForURL(TEST_URL);
-  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
-  let {testActor} = inspector;
+  let env = await openInspectorForURL(TEST_URL);
+  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+  let {testActor, inspector} = env;
+  let view = selectRuleView(inspector);
+  let highlighters = view.highlighters;
 
-  await testPolygonMovePoint(testActor, helper);
-  await testPolygonAddPoint(testActor, helper);
-  await testPolygonRemovePoint(testActor, helper);
-  await testCircleMoveCenter(testActor, helper);
-  await testEllipseMoveRadius(testActor, helper);
-  await testInsetMoveEdges(testActor, helper);
+  let config = {inspector, view, highlighters, testActor, helper};
+
+  await testPolygonMovePoint(config);
+  await testPolygonAddPoint(config);
+  await testPolygonRemovePoint(config);
+  await testCircleMoveCenter(config);
+  await testEllipseMoveRadius(config);
+  await testInsetMoveEdges(config);
 
   helper.finalize();
 });
 
-async function testPolygonMovePoint(testActor, helper) {
-  info("Displaying polygon");
-  await helper.show("#polygon", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function getComputedPropertyValue(selector, property, inspector) {
+  let highlightedNode = await getNodeFront(selector, inspector);
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
+  return computedStyle[property].value;
+}
+
+async function setup(config) {
+  const { view, selector, property, inspector } = config;
+  info(`Turn on shapes highlighter for ${selector}`);
+  await selectNode(selector, inspector);
+  await toggleShapesHighlighter(view, selector, property, true);
+}
 
-  let points = await helper.getElementAttribute("shapes-polygon", "points");
+async function teardown(config) {
+  const { view, selector, property } = config;
+  info(`Turn off shapes highlighter for ${selector}`);
+  await toggleShapesHighlighter(view, selector, property, false);
+}
+
+async function testPolygonMovePoint(config) {
+  const {inspector, view, highlighters, testActor, helper} = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  await setup({selector, property, ...config});
+
+  let points = await testActor.getHighlighterNodeAttribute(
+    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let [x, y] = points.split(" ")[0].split(",");
-  let quads = await testActor.getAllAdjustedQuads("#polygon");
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { top, left, width, height } = quads.border[0].bounds;
   x = left + width * x / 100;
   y = top + height * y / 100;
   let dx = width / 10;
   let dy = height / 10;
 
+  let onRuleViewChanged = view.once("ruleview-changed");
   info("Moving first polygon point");
+  let { mouse } = helper;
   await mouse.down(x, y);
   await mouse.move(x + dx, y + dy);
   await mouse.up();
   await testActor.reflow();
+  info("Waiting for rule view changed from shape change");
+  await onRuleViewChanged;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`${dx}px ${dy}px`), `Point moved to ${dx}px ${dy}px`);
+
+  await teardown({selector, property, ...config});
 }
 
-async function testPolygonAddPoint(testActor, helper) {
-  await helper.show("#polygon", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function testPolygonAddPoint(config) {
+  const {inspector, view, highlighters, testActor, helper} = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  await setup({selector, property, ...config});
 
   // Move first point to have same x as second point, then double click between
   // the two points to add a new one.
-  let points = await helper.getElementAttribute("shapes-polygon", "points");
+  let points = await testActor.getHighlighterNodeAttribute(
+    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let pointsArray = points.split(" ");
-  let quads = await testActor.getAllAdjustedQuads("#polygon");
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { top, left, width, height } = quads.border[0].bounds;
   let [x1, y1] = pointsArray[0].split(",");
   let [x2, y2] = pointsArray[1].split(",");
   x1 = left + width * x1 / 100;
   x2 = left + width * x2 / 100;
   y1 = top + height * y1 / 100;
   y2 = top + height * y2 / 100;
 
+  let { mouse } = helper;
   await mouse.down(x1, y1);
   await mouse.move(x2, y1);
   await mouse.up();
   await testActor.reflow();
 
   let newPointX = x2;
   let newPointY = (y1 + y2) / 2;
   let options = {
     selector: ":root",
     x: newPointX,
     y: newPointY,
     center: false,
     options: {clickCount: 2}
   };
 
+  let onRuleViewChanged = view.once("ruleview-changed");
   info("Adding new polygon point");
   await testActor.synthesizeMouse(options);
   await testActor.reflow();
+  info("Waiting for rule view changed from shape change");
+  await onRuleViewChanged;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
   // Decimal precision for coordinates with percentage units is 2
   let precision = 2;
   // Round to the desired decimal precision and cast to Number to remove trailing zeroes.
   newPointX = Number((newPointX * 100 / width).toFixed(precision));
   newPointY = Number((newPointY * 100 / height).toFixed(precision));
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`${newPointX}% ${newPointY}%`),
      "Point successfuly added");
+
+  await teardown({selector, property, ...config});
 }
 
-async function testPolygonRemovePoint(testActor, helper) {
-  await helper.show("#polygon", {mode: "cssClipPath"});
-  let { highlightedNode } = helper;
+async function testPolygonRemovePoint(config) {
+  const {inspector, highlighters, testActor, helper} = config;
+  const selector = "#polygon";
+  const property = "clip-path";
 
-  let points = await helper.getElementAttribute("shapes-polygon", "points");
+  await setup({selector, property, ...config});
+
+  let points = await testActor.getHighlighterNodeAttribute(
+    "shapes-polygon", "points", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let [x, y] = points.split(" ")[0].split(",");
-  let quads = await testActor.getAllAdjustedQuads("#polygon");
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { top, left, width, height } = quads.border[0].bounds;
 
   let options = {
     selector: ":root",
     x: left + width * x / 100,
     y: top + height * y / 100,
     center: false,
     options: {clickCount: 2}
   };
 
-  info("Removing first polygon point");
-  await testActor.synthesizeMouse(options);
-  await testActor.reflow();
+  info("Move mouse over first point in highlighter");
+  let onEventHandled = highlighters.once("highlighter-event-handled");
+  let { mouse } = helper;
+  await mouse.move(options.x, options.y);
+  await onEventHandled;
+  let markerHidden = await testActor.getHighlighterNodeAttribute(
+    "shapes-marker-hover", "hidden", highlighters.highlighters[HIGHLIGHTER_TYPE]);
+  ok(!markerHidden, "Marker on highlighter is visible");
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  info("Double click on first point in highlighter");
+  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+  await testActor.synthesizeMouse(options);
+  info("Waiting for shape changes to apply");
+  await onShapeChangeApplied;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(!definition.includes(`${x}% ${y}%`), "Point successfully removed");
+
+  await teardown({selector, property, ...config});
 }
 
-async function testCircleMoveCenter(testActor, helper) {
-  await helper.show("#circle", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function testCircleMoveCenter(config) {
+  const {inspector, highlighters, testActor, helper} = config;
+  const selector = "#circle";
+  const property = "clip-path";
 
-  let cx = parseFloat(await helper.getElementAttribute("shapes-ellipse", "cx"));
-  let cy = parseFloat(await helper.getElementAttribute("shapes-ellipse", "cy"));
-  let quads = await testActor.getAllAdjustedQuads("#circle");
+  await setup({selector, property, ...config});
+
+  let cx = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let cy = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cy", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { width, height } = quads.border[0].bounds;
   let cxPixel = width * cx / 100;
   let cyPixel = height * cy / 100;
   let dx = width / 10;
   let dy = height / 10;
 
+  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
   info("Moving circle center");
-  await mouse.down(cxPixel, cyPixel, "#circle");
-  await mouse.move(cxPixel + dx, cyPixel + dy, "#circle");
-  await mouse.up(cxPixel + dx, cyPixel + dy, "#circle");
+  let { mouse } = helper;
+  await mouse.down(cxPixel, cyPixel, selector);
+  await mouse.move(cxPixel + dx, cyPixel + dy, selector);
+  await mouse.up(cxPixel + dx, cyPixel + dy, selector);
   await testActor.reflow();
+  await onShapeChangeApplied;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`at ${cx + 10}% ${cy + 10}%`),
      "Circle center successfully moved");
+
+  await teardown({selector, property, ...config});
 }
 
-async function testEllipseMoveRadius(testActor, helper) {
-  await helper.show("#ellipse", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function testEllipseMoveRadius(config) {
+  const {inspector, highlighters, testActor, helper} = config;
+  const selector = "#ellipse";
+  const property = "clip-path";
+
+  await setup({selector, property, ...config});
 
-  let rx = parseFloat(await helper.getElementAttribute("shapes-ellipse", "rx"));
-  let ry = parseFloat(await helper.getElementAttribute("shapes-ellipse", "ry"));
-  let cx = parseFloat(await helper.getElementAttribute("shapes-ellipse", "cx"));
-  let cy = parseFloat(await helper.getElementAttribute("shapes-ellipse", "cy"));
+  let rx = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "rx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let ry = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "ry", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let cx = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cx", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let cy = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cy", highlighters.highlighters[HIGHLIGHTER_TYPE]));
   let quads = await testActor.getAllAdjustedQuads("#ellipse");
   let { width, height } = quads.content[0].bounds;
-  let computedStyle = await highlightedNode.getComputedStyle();
+  let highlightedNode = await getNodeFront(selector, inspector);
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   let paddingTop = parseFloat(computedStyle["padding-top"].value);
   let paddingLeft = parseFloat(computedStyle["padding-left"].value);
   let cxPixel = paddingLeft + width * cx / 100;
   let cyPixel = paddingTop + height * cy / 100;
   let rxPixel = cxPixel + width * rx / 100;
   let ryPixel = cyPixel + height * ry / 100;
   let dx = width / 10;
   let dy = height / 10;
 
+  let { mouse } = helper;
   info("Moving ellipse rx");
-  await mouse.down(rxPixel, cyPixel, "#ellipse");
-  await mouse.move(rxPixel + dx, cyPixel, "#ellipse");
-  await mouse.up(rxPixel + dx, cyPixel, "#ellipse");
+  await mouse.down(rxPixel, cyPixel, selector);
+  await mouse.move(rxPixel + dx, cyPixel, selector);
+  await mouse.up(rxPixel + dx, cyPixel, selector);
   await testActor.reflow();
 
   info("Moving ellipse ry");
-  await mouse.down(cxPixel, ryPixel, "#ellipse");
-  await mouse.move(cxPixel, ryPixel - dy, "#ellipse");
-  await mouse.up(cxPixel, ryPixel - dy, "#ellipse");
+  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+  await mouse.down(cxPixel, ryPixel, selector);
+  await mouse.move(cxPixel, ryPixel - dy, selector);
+  await mouse.up(cxPixel, ryPixel - dy, selector);
   await testActor.reflow();
+  await onShapeChangeApplied;
 
-  computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(`${rx + 10}% ${ry - 10}%`),
      "Ellipse radiuses successfully moved");
+
+  await teardown({selector, property, ...config});
 }
 
-async function testInsetMoveEdges(testActor, helper) {
-  await helper.show("#inset", {mode: "cssClipPath"});
-  let { mouse, highlightedNode } = helper;
+async function testInsetMoveEdges(config) {
+  const {inspector, highlighters, testActor, helper} = config;
+  const selector = "#inset";
+  const property = "clip-path";
+
+  await setup({selector, property, ...config});
 
-  let x = parseFloat(await helper.getElementAttribute("shapes-rect", "x"));
-  let y = parseFloat(await helper.getElementAttribute("shapes-rect", "y"));
-  let width = parseFloat(await helper.getElementAttribute("shapes-rect", "width"));
-  let height = parseFloat(await helper.getElementAttribute("shapes-rect", "height"));
-  let quads = await testActor.getAllAdjustedQuads("#inset");
+  let x = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "x", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let y = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "y", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let width = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "width", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let height = parseFloat(await testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "height", highlighters.highlighters[HIGHLIGHTER_TYPE]));
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { width: elemWidth, height: elemHeight } = quads.content[0].bounds;
 
   let left = elemWidth * x / 100;
   let top = elemHeight * y / 100;
   let right = left + elemWidth * width / 100;
   let bottom = top + elemHeight * height / 100;
   let xCenter = (left + right) / 2;
   let yCenter = (top + bottom) / 2;
   let dx = elemWidth / 10;
   let dy = elemHeight / 10;
+  let { mouse } = helper;
 
   info("Moving inset top");
-  await mouse.down(xCenter, top, "#inset");
-  await mouse.move(xCenter, top + dy, "#inset");
-  await mouse.up(xCenter, top + dy, "#inset");
+  await mouse.down(xCenter, top, selector);
+  await mouse.move(xCenter, top + dy, selector);
+  await mouse.up(xCenter, top + dy, selector);
   await testActor.reflow();
 
   info("Moving inset bottom");
-  await mouse.down(xCenter, bottom, "#inset");
-  await mouse.move(xCenter, bottom + dy, "#inset");
-  await mouse.up(xCenter, bottom + dy, "#inset");
+  await mouse.down(xCenter, bottom, selector);
+  await mouse.move(xCenter, bottom + dy, selector);
+  await mouse.up(xCenter, bottom + dy, selector);
   await testActor.reflow();
 
   info("Moving inset left");
-  await mouse.down(left, yCenter, "#inset");
-  await mouse.move(left + dx, yCenter, "#inset");
-  await mouse.up(left + dx, yCenter, "#inset");
+  await mouse.down(left, yCenter, selector);
+  await mouse.move(left + dx, yCenter, selector);
+  await mouse.up(left + dx, yCenter, selector);
   await testActor.reflow();
 
   info("Moving inset right");
-  await mouse.down(right, yCenter, "#inset");
-  await mouse.move(right + dx, yCenter, "#inset");
-  await mouse.up(right + dx, yCenter, "#inset");
+  let onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+  await mouse.down(right, yCenter, selector);
+  await mouse.move(right + dx, yCenter, selector);
+  await mouse.up(right + dx, yCenter, selector);
   await testActor.reflow();
+  await onShapeChangeApplied;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
-  let definition = computedStyle["clip-path"].value;
+  let definition = await getComputedPropertyValue(selector, property, inspector);
   ok(definition.includes(
     `${top + dy}px ${elemWidth - right - dx}px ${100 - y - height - 10}% ${x + 10}%`),
      "Inset edges successfully moved");
+
+  await teardown({selector, property, ...config});
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js
@@ -2,109 +2,114 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test hovering over shape points in the rule-view and shapes highlighter.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
-
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
-const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
 
 add_task(async function() {
-  await pushPref(CSS_SHAPES_ENABLED_PREF, true);
   let env = await openInspectorForURL(TEST_URL);
   let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
   let { testActor, inspector } = env;
   let view = selectRuleView(inspector);
   let highlighters = view.highlighters;
+  let config = { inspector, view, highlighters, testActor, helper };
 
-  await highlightFromRuleView(inspector, view, highlighters, testActor);
-  await highlightFromHighlighter(view, highlighters, testActor, helper);
+  await highlightFromRuleView(config);
+  await highlightFromHighlighter(config);
 });
 
-async function highlightFromRuleView(inspector, view, highlighters, testActor) {
-  await selectNode("#polygon", inspector);
-  await toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true);
-  let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan;
+async function setup(config) {
+  const { view, selector, property, inspector } = config;
+  info(`Turn on shapes highlighter for ${selector}`);
+  await selectNode(selector, inspector);
+  await toggleShapesHighlighter(view, selector, property, true);
+}
+
+async function teardown(config) {
+  const { view, selector, property } = config;
+  info(`Turn off shapes highlighter for ${selector}`);
+  await toggleShapesHighlighter(view, selector, property, false);
+}
+/*
+* Test that points hovered in the rule view will highlight corresponding points
+* in the shapes highlighter on the page.
+*/
+async function highlightFromRuleView(config) {
+  const { view, highlighters, testActor } = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  await setup({ selector, property, ...config });
+
+  let container = getRuleViewProperty(view, selector, property).valueSpan;
   let shapesToggle = container.querySelector(".ruleview-shapeswatch");
 
   let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE];
   let markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(markerHidden, "Hover marker on highlighter is not visible");
 
   info("Hover over point 0 in rule view");
   let pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
   let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   EventUtils.synthesizeMouseAtCenter(pointSpan, {type: "mousemove"}, view.styleWindow);
   await onHighlighterShown;
 
-  ok(pointSpan.classList.contains("active"), "Hovered span is active");
-  is(highlighters.state.shapes.options.hoverPoint, "0",
-     "Hovered point is saved to state");
-
+  info("Point in shapes highlighter is marked when same point in rule view is hovered");
   markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(!markerHidden, "Marker on highlighter is visible");
 
   info("Move mouse off point");
   onHighlighterShown = highlighters.once("shapes-highlighter-shown");
   EventUtils.synthesizeMouseAtCenter(shapesToggle, {type: "mousemove"}, view.styleWindow);
   await onHighlighterShown;
 
-  ok(!pointSpan.classList.contains("active"), "Hovered span is no longer active");
-  is(highlighters.state.shapes.options.hoverPoint, null, "Hovered point is null");
-
   markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(markerHidden, "Marker on highlighter is not visible");
 
-  info("Hide shapes highlighter");
-  await toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", false);
+  await teardown({selector, property, ...config});
 }
 
-async function highlightFromHighlighter(view, highlighters, testActor, helper) {
+/*
+* Test that points hovered in the shapes highlighter on the page will highlight
+* corresponding points in the rule view.
+*/
+async function highlightFromHighlighter(config) {
+  const { view, highlighters, testActor, helper } = config;
+  const selector = "#polygon";
+  const property = "clip-path";
+
+  await setup({ selector, property, ...config });
+
   let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE];
   let { mouse } = helper;
-
-  await toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true);
-  let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan;
+  let container = getRuleViewProperty(view, selector, property).valueSpan;
 
   info("Hover over first point in highlighter");
   let onEventHandled = highlighters.once("highlighter-event-handled");
   await mouse.move(0, 0);
   await onEventHandled;
   let markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(!markerHidden, "Marker on highlighter is visible");
 
+  info("Point in rule view is marked when same point in shapes highlighter is hovered");
   let pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
   ok(pointSpan.classList.contains("active"), "Span for point 0 is active");
-  is(highlighters.state.shapes.hoverPoint, "0", "Hovered point is saved to state");
-
-  info("Check that point is still highlighted after moving it");
-  await mouse.down(0, 0);
-  await mouse.move(10, 10);
-  await mouse.up(10, 10);
-  markerHidden = await testActor.getHighlighterNodeAttribute(
-    "shapes-marker-hover", "hidden", highlighterFront);
-  ok(!markerHidden, "Marker on highlighter is visible after moving point");
-
-  container = getRuleViewProperty(view, "element", "clip-path").valueSpan;
-  pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
-  ok(pointSpan.classList.contains("active"),
-     "Span for point 0 is active after moving point");
-  is(highlighters.state.shapes.hoverPoint, "0",
-     "Hovered point is saved to state after moving point");
 
   info("Move mouse off point");
   onEventHandled = highlighters.once("highlighter-event-handled");
   await mouse.move(100, 100);
   await onEventHandled;
   markerHidden = await testActor.getHighlighterNodeAttribute(
     "shapes-marker-hover", "hidden", highlighterFront);
   ok(markerHidden, "Marker on highlighter is no longer visible");
   ok(!pointSpan.classList.contains("active"), "Span for point 0 is no longer active");
-  is(highlighters.state.shapes.hoverPoint, null, "Hovered point is null");
+
+  await teardown({ selector, property, ...config });
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_06.js
@@ -3,145 +3,183 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // Test that shapes are updated correctly on mouse events in transform mode.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
-const SHAPE_IDS = ["#polygon-transform", "#circle", "#ellipse", "#inset"];
+const SHAPE_SELECTORS = ["#polygon-transform", "#circle", "#ellipse", "#inset"];
 
 add_task(async function() {
-  let inspector = await openInspectorForURL(TEST_URL);
-  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
-  let {testActor} = inspector;
+  let env = await openInspectorForURL(TEST_URL);
+  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+  let {testActor, inspector} = env;
+  let view = selectRuleView(inspector);
+  let highlighters = view.highlighters;
+  let config = { inspector, view, highlighters, testActor, helper };
 
-  await testTranslate(testActor, helper);
-  await testScale(testActor, helper);
-
-  helper.finalize();
+  await testTranslate(config);
+  await testScale(config);
 });
 
-async function testTranslate(testActor, helper) {
-  for (let shape of SHAPE_IDS) {
-    info(`Displaying ${shape}`);
-    await helper.show(shape, {mode: "cssClipPath", transformMode: true});
+async function setup(config) {
+  const { inspector, view, selector, property, options } = config;
+  await selectNode(selector, inspector);
+  await toggleShapesHighlighter(view, selector, property, true, options);
+}
+
+async function teardown(config) {
+  const { view, selector, property } = config;
+  info(`Turn off shapes highlighter for ${selector}`);
+  await toggleShapesHighlighter(view, selector, property, false);
+}
+
+async function testTranslate(config) {
+  const { testActor, helper, highlighters } = config;
+  const options = { transformMode: true };
+  const property = "clip-path";
+
+  for (let selector of SHAPE_SELECTORS) {
+    await setup({selector, property, options, ...config});
     let { mouse } = helper;
 
-    let { center, width, height } = await getBoundingBoxInPx(testActor, helper, shape);
+    let { center, width, height } = await getBoundingBoxInPx({selector, ...config});
     let [x, y] = center;
     let dx = width / 10;
     let dy = height / 10;
+    let onShapeChangeApplied;
 
-    info(`Translating ${shape}`);
-    await mouse.down(x, y, shape);
-    await mouse.move(x + dx, y + dy, shape);
-    await mouse.up(x + dx, y + dy, shape);
-    await testActor.reflow();
+    info(`Translating ${selector}`);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(x, y, selector);
+    await mouse.move(x + dx, y + dy, selector);
+    await mouse.up(x + dx, y + dy, selector);
+    await onShapeChangeApplied;
+
+    let newBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(newBB.center[0], x, `${selector} translated on y axis`);
+    isnot(newBB.center[1], y, `${selector} translated on x axis`);
 
-    let newBB = await getBoundingBoxInPx(testActor, helper);
-    isnot(newBB.center[0], x, `${shape} translated on y axis`);
-    isnot(newBB.center[1], y, `${shape} translated on x axis`);
+    info(`Translating ${selector} back`);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(x + dx, y + dy, selector);
+    await mouse.move(x, y, selector);
+    await mouse.up(x, y, selector);
+    await testActor.reflow();
+    await onShapeChangeApplied;
 
-    info(`Translating ${shape} back`);
-    await mouse.down(x + dx, y + dy, shape);
-    await mouse.move(x, y, shape);
-    await mouse.up(x, y, shape);
-    await testActor.reflow();
+    newBB = await getBoundingBoxInPx({selector, ...config});
+    is(newBB.center[0], x, `${selector} translated back on x axis`);
+    is(newBB.center[1], y, `${selector} translated back on y axis`);
 
-    newBB = await getBoundingBoxInPx(testActor, helper, shape);
-    is(newBB.center[0], x, `${shape} translated back on x axis`);
-    is(newBB.center[1], y, `${shape} translated back on y axis`);
+    await teardown({selector, property, ...config});
   }
 }
 
-async function testScale(testActor, helper) {
-  for (let shape of SHAPE_IDS) {
-    info(`Displaying ${shape}`);
-    await helper.show(shape, {mode: "cssClipPath", transformMode: true});
+async function testScale(config) {
+  const { testActor, helper, highlighters } = config;
+  const options = { transformMode: true };
+  const property = "clip-path";
+
+  for (let selector of SHAPE_SELECTORS) {
+    await setup({selector, property, options, ...config});
     let { mouse } = helper;
 
     let { nw, width,
-          height, center } = await getBoundingBoxInPx(testActor, helper, shape);
+          height, center } = await getBoundingBoxInPx({selector, ...config});
 
     // if the top or left edges are not visible, move the shape so it is.
     if (nw[0] < 0 || nw[1] < 0) {
       let [x, y] = center;
       let dx = Math.max(0, -nw[0]);
       let dy = Math.max(0, -nw[1]);
-      await mouse.down(x, y, shape);
-      await mouse.move(x + dx, y + dy, shape);
-      await mouse.up(x + dx, y + dy, shape);
+      await mouse.down(x, y, selector);
+      await mouse.move(x + dx, y + dy, selector);
+      await mouse.up(x + dx, y + dy, selector);
       await testActor.reflow();
       nw[0] += dx;
       nw[1] += dy;
     }
     let dx = width / 10;
     let dy = height / 10;
+    let onShapeChangeApplied;
 
     info("Scaling from nw");
-    await mouse.down(nw[0], nw[1], shape);
-    await mouse.move(nw[0] + dx, nw[1] + dy, shape);
-    await mouse.up(nw[0] + dx, nw[1] + dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(nw[0], nw[1], selector);
+    await mouse.move(nw[0] + dx, nw[1] + dy, selector);
+    await mouse.up(nw[0] + dx, nw[1] + dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let nwBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(nwBB.nw[0], nw[0], `${shape} nw moved right after nw scale`);
-    isnot(nwBB.nw[1], nw[1], `${shape} nw moved down after nw scale`);
-    isnot(nwBB.width, width, `${shape} width reduced after nw scale`);
-    isnot(nwBB.height, height, `${shape} height reduced after nw scale`);
+    let nwBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(nwBB.nw[0], nw[0], `${selector} nw moved right after nw scale`);
+    isnot(nwBB.nw[1], nw[1], `${selector} nw moved down after nw scale`);
+    isnot(nwBB.width, width, `${selector} width reduced after nw scale`);
+    isnot(nwBB.height, height, `${selector} height reduced after nw scale`);
 
     info("Scaling from ne");
-    await mouse.down(nwBB.ne[0], nwBB.ne[1], shape);
-    await mouse.move(nwBB.ne[0] - dx, nwBB.ne[1] + dy, shape);
-    await mouse.up(nwBB.ne[0] - dx, nwBB.ne[1] + dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(nwBB.ne[0], nwBB.ne[1], selector);
+    await mouse.move(nwBB.ne[0] - dx, nwBB.ne[1] + dy, selector);
+    await mouse.up(nwBB.ne[0] - dx, nwBB.ne[1] + dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let neBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(neBB.ne[0], nwBB.ne[0], `${shape} ne moved right after ne scale`);
-    isnot(neBB.ne[1], nwBB.ne[1], `${shape} ne moved down after ne scale`);
-    isnot(neBB.width, nwBB.width, `${shape} width reduced after ne scale`);
-    isnot(neBB.height, nwBB.height, `${shape} height reduced after ne scale`);
+    let neBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(neBB.ne[0], nwBB.ne[0], `${selector} ne moved right after ne scale`);
+    isnot(neBB.ne[1], nwBB.ne[1], `${selector} ne moved down after ne scale`);
+    isnot(neBB.width, nwBB.width, `${selector} width reduced after ne scale`);
+    isnot(neBB.height, nwBB.height, `${selector} height reduced after ne scale`);
 
     info("Scaling from sw");
-    await mouse.down(neBB.sw[0], neBB.sw[1], shape);
-    await mouse.move(neBB.sw[0] + dx, neBB.sw[1] - dy, shape);
-    await mouse.up(neBB.sw[0] + dx, neBB.sw[1] - dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(neBB.sw[0], neBB.sw[1], selector);
+    await mouse.move(neBB.sw[0] + dx, neBB.sw[1] - dy, selector);
+    await mouse.up(neBB.sw[0] + dx, neBB.sw[1] - dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let swBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(swBB.sw[0], neBB.sw[0], `${shape} sw moved right after sw scale`);
-    isnot(swBB.sw[1], neBB.sw[1], `${shape} sw moved down after sw scale`);
-    isnot(swBB.width, neBB.width, `${shape} width reduced after sw scale`);
-    isnot(swBB.height, neBB.height, `${shape} height reduced after sw scale`);
+    let swBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(swBB.sw[0], neBB.sw[0], `${selector} sw moved right after sw scale`);
+    isnot(swBB.sw[1], neBB.sw[1], `${selector} sw moved down after sw scale`);
+    isnot(swBB.width, neBB.width, `${selector} width reduced after sw scale`);
+    isnot(swBB.height, neBB.height, `${selector} height reduced after sw scale`);
 
     info("Scaling from se");
-    await mouse.down(swBB.se[0], swBB.se[1], shape);
-    await mouse.move(swBB.se[0] - dx, swBB.se[1] - dy, shape);
-    await mouse.up(swBB.se[0] - dx, swBB.se[1] - dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(swBB.se[0], swBB.se[1], selector);
+    await mouse.move(swBB.se[0] - dx, swBB.se[1] - dy, selector);
+    await mouse.up(swBB.se[0] - dx, swBB.se[1] - dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let seBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(seBB.se[0], swBB.se[0], `${shape} se moved right after se scale`);
-    isnot(seBB.se[1], swBB.se[1], `${shape} se moved down after se scale`);
-    isnot(seBB.width, swBB.width, `${shape} width reduced after se scale`);
-    isnot(seBB.height, swBB.height, `${shape} height reduced after se scale`);
+    let seBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(seBB.se[0], swBB.se[0], `${selector} se moved right after se scale`);
+    isnot(seBB.se[1], swBB.se[1], `${selector} se moved down after se scale`);
+    isnot(seBB.width, swBB.width, `${selector} width reduced after se scale`);
+    isnot(seBB.height, swBB.height, `${selector} height reduced after se scale`);
+
+    await teardown({selector, property, ...config});
   }
 }
 
-async function getBoundingBoxInPx(testActor, helper, shape = "#polygon") {
-  let quads = await testActor.getAllAdjustedQuads(shape);
+async function getBoundingBoxInPx(config) {
+  const { testActor, selector, inspector, highlighters } = config;
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { width, height } = quads.content[0].bounds;
-  let computedStyle = await helper.highlightedNode.getComputedStyle();
+  let highlightedNode = await getNodeFront(selector, inspector);
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   let paddingTop = parseFloat(computedStyle["padding-top"].value);
   let paddingLeft = parseFloat(computedStyle["padding-left"].value);
-
   // path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers
-  let path = await helper.getElementAttribute("shapes-bounding-box", "d");
+  let path = await testActor.getHighlighterNodeAttribute(
+    "shapes-bounding-box", "d", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let coords = path.replace(/[MLZ]/g, "").split(" ").map((n, i) => {
     return i % 2 === 0 ? paddingLeft + width * n / 100 : paddingTop + height * n / 100;
   });
 
   let nw = [coords[0], coords[1]];
   let ne = [coords[2], coords[3]];
   let se = [coords[4], coords[5]];
   let sw = [coords[6], coords[7]];
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_07.js
@@ -3,111 +3,140 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // Test that shapes are updated correctly for scaling on one axis in transform mode.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
-const SHAPE_IDS = ["#polygon-transform", "#ellipse"];
+const SHAPE_SELECTORS = ["#polygon-transform", "#ellipse"];
 
 add_task(async function() {
-  let inspector = await openInspectorForURL(TEST_URL);
-  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
-  let {testActor} = inspector;
+  let env = await openInspectorForURL(TEST_URL);
+  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+  let {testActor, inspector} = env;
+  let view = selectRuleView(inspector);
+  let highlighters = view.highlighters;
+  let config = { inspector, view, highlighters, testActor, helper };
 
-  await testOneDimScale(testActor, helper);
-
-  helper.finalize();
+  await testOneDimScale(config);
 });
 
-async function testOneDimScale(testActor, helper) {
-  for (let shape of SHAPE_IDS) {
-    info(`Displaying ${shape}`);
-    await helper.show(shape, {mode: "cssClipPath", transformMode: true});
+async function setup(config) {
+  const { inspector, view, selector, property, options } = config;
+  await selectNode(selector, inspector);
+  await toggleShapesHighlighter(view, selector, property, true, options);
+}
+
+async function teardown(config) {
+  const { view, selector, property } = config;
+  info(`Turn off shapes highlighter for ${selector}`);
+  await toggleShapesHighlighter(view, selector, property, false);
+}
+
+async function testOneDimScale(config) {
+  const { testActor, helper, highlighters } = config;
+  const options = { transformMode: true };
+  const property = "clip-path";
+
+  for (let selector of SHAPE_SELECTORS) {
+    await setup({selector, property, options, ...config});
     let { mouse } = helper;
 
     let { nw, width,
-          height, center } = await getBoundingBoxInPx(testActor, helper, shape);
+          height, center } = await getBoundingBoxInPx({selector, ...config});
 
     // if the top or left edges are not visible, move the shape so it is.
     if (nw[0] < 0 || nw[1] < 0) {
       let [x, y] = center;
       let dx = Math.max(0, -nw[0]);
       let dy = Math.max(0, -nw[1]);
-      await mouse.down(x, y, shape);
-      await mouse.move(x + dx, y + dy, shape);
-      await mouse.up(x + dx, y + dy, shape);
+      await mouse.down(x, y, selector);
+      await mouse.move(x + dx, y + dy, selector);
+      await mouse.up(x + dx, y + dy, selector);
       await testActor.reflow();
       nw[0] += dx;
       nw[1] += dy;
     }
     let dx = width / 10;
     let dy = height / 10;
+    let onShapeChangeApplied;
 
     info("Scaling from w");
-    await mouse.down(nw[0], center[1], shape);
-    await mouse.move(nw[0] + dx, center[1], shape);
-    await mouse.up(nw[0] + dx, center[1], shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(nw[0], center[1], selector);
+    await mouse.move(nw[0] + dx, center[1], selector);
+    await mouse.up(nw[0] + dx, center[1], selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let wBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(wBB.nw[0], nw[0], `${shape} nw moved right after w scale`);
-    is(wBB.nw[1], nw[1], `${shape} nw not moved down after w scale`);
-    isnot(wBB.width, width, `${shape} width reduced after w scale`);
-    is(wBB.height, height, `${shape} height not reduced after w scale`);
+    let wBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(wBB.nw[0], nw[0], `${selector} nw moved right after w scale`);
+    is(wBB.nw[1], nw[1], `${selector} nw not moved down after w scale`);
+    isnot(wBB.width, width, `${selector} width reduced after w scale`);
+    is(wBB.height, height, `${selector} height not reduced after w scale`);
 
     info("Scaling from e");
-    await mouse.down(wBB.ne[0], center[1], shape);
-    await mouse.move(wBB.ne[0] - dx, center[1], shape);
-    await mouse.up(wBB.ne[0] - dx, center[1], shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(wBB.ne[0], center[1], selector);
+    await mouse.move(wBB.ne[0] - dx, center[1], selector);
+    await mouse.up(wBB.ne[0] - dx, center[1], selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let eBB = await getBoundingBoxInPx(testActor, helper, shape);
-    isnot(eBB.ne[0], wBB.ne[0], `${shape} ne moved left after e scale`);
-    is(eBB.ne[1], wBB.ne[1], `${shape} ne not moved down after e scale`);
-    isnot(eBB.width, wBB.width, `${shape} width reduced after e scale`);
-    is(eBB.height, wBB.height, `${shape} height not reduced after e scale`);
+    let eBB = await getBoundingBoxInPx({selector, ...config});
+    isnot(eBB.ne[0], wBB.ne[0], `${selector} ne moved left after e scale`);
+    is(eBB.ne[1], wBB.ne[1], `${selector} ne not moved down after e scale`);
+    isnot(eBB.width, wBB.width, `${selector} width reduced after e scale`);
+    is(eBB.height, wBB.height, `${selector} height not reduced after e scale`);
 
     info("Scaling from s");
-    await mouse.down(eBB.center[0], eBB.sw[1], shape);
-    await mouse.move(eBB.center[0], eBB.sw[1] - dy, shape);
-    await mouse.up(eBB.center[0], eBB.sw[1] - dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(eBB.center[0], eBB.sw[1], selector);
+    await mouse.move(eBB.center[0], eBB.sw[1] - dy, selector);
+    await mouse.up(eBB.center[0], eBB.sw[1] - dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let sBB = await getBoundingBoxInPx(testActor, helper, shape);
-    is(sBB.sw[0], eBB.sw[0], `${shape} sw not moved right after w scale`);
-    isnot(sBB.sw[1], eBB.sw[1], `${shape} sw moved down after w scale`);
-    is(sBB.width, eBB.width, `${shape} width not reduced after w scale`);
-    isnot(sBB.height, eBB.height, `${shape} height reduced after w scale`);
+    let sBB = await getBoundingBoxInPx({selector, ...config});
+    is(sBB.sw[0], eBB.sw[0], `${selector} sw not moved right after w scale`);
+    isnot(sBB.sw[1], eBB.sw[1], `${selector} sw moved down after w scale`);
+    is(sBB.width, eBB.width, `${selector} width not reduced after w scale`);
+    isnot(sBB.height, eBB.height, `${selector} height reduced after w scale`);
 
     info("Scaling from n");
-    await mouse.down(sBB.center[0], sBB.nw[1], shape);
-    await mouse.move(sBB.center[0], sBB.nw[1] + dy, shape);
-    await mouse.up(sBB.center[0], sBB.nw[1] + dy, shape);
+    onShapeChangeApplied = highlighters.once("shapes-highlighter-changes-applied");
+    await mouse.down(sBB.center[0], sBB.nw[1], selector);
+    await mouse.move(sBB.center[0], sBB.nw[1] + dy, selector);
+    await mouse.up(sBB.center[0], sBB.nw[1] + dy, selector);
     await testActor.reflow();
+    await onShapeChangeApplied;
 
-    let nBB = await getBoundingBoxInPx(testActor, helper, shape);
-    is(nBB.nw[0], sBB.nw[0], `${shape} nw not moved right after n scale`);
-    isnot(nBB.nw[1], sBB.nw[1], `${shape} nw moved down after n scale`);
-    is(nBB.width, sBB.width, `${shape} width reduced after n scale`);
-    isnot(nBB.height, sBB.height, `${shape} height not reduced after n scale`);
+    let nBB = await getBoundingBoxInPx({selector, ...config});
+    is(nBB.nw[0], sBB.nw[0], `${selector} nw not moved right after n scale`);
+    isnot(nBB.nw[1], sBB.nw[1], `${selector} nw moved down after n scale`);
+    is(nBB.width, sBB.width, `${selector} width reduced after n scale`);
+    isnot(nBB.height, sBB.height, `${selector} height not reduced after n scale`);
+
+    await teardown({selector, property, ...config});
   }
 }
 
-async function getBoundingBoxInPx(testActor, helper, shape = "#polygon") {
-  let quads = await testActor.getAllAdjustedQuads(shape);
+async function getBoundingBoxInPx(config) {
+  const { testActor, selector, inspector, highlighters } = config;
+  let quads = await testActor.getAllAdjustedQuads(selector);
   let { width, height } = quads.content[0].bounds;
-  let computedStyle = await helper.highlightedNode.getComputedStyle();
+  let highlightedNode = await getNodeFront(selector, inspector);
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   let paddingTop = parseFloat(computedStyle["padding-top"].value);
   let paddingLeft = parseFloat(computedStyle["padding-left"].value);
-
   // path is always of form "Mx y Lx y Lx y Lx y Z", where x/y are numbers
-  let path = await helper.getElementAttribute("shapes-bounding-box", "d");
+  let path = await testActor.getHighlighterNodeAttribute(
+    "shapes-bounding-box", "d", highlighters.highlighters[HIGHLIGHTER_TYPE]);
   let coords = path.replace(/[MLZ]/g, "").split(" ").map((n, i) => {
     return i % 2 === 0 ? paddingLeft + width * n / 100 : paddingTop + height * n / 100;
   });
 
   let nw = [coords[0], coords[1]];
   let ne = [coords[2], coords[3]];
   let se = [coords[4], coords[5]];
   let sw = [coords[6], coords[7]];
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_iframe_01.js
@@ -5,42 +5,55 @@
 "use strict";
 
 // Test that shapes in iframes are updated correctly on mouse events.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes_iframe.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
 
 add_task(async function() {
-  let inspector = await openInspectorForURL(TEST_URL);
-  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(inspector);
-  let {testActor} = inspector;
+  let env = await openInspectorForURL(TEST_URL);
+  let helper = await getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+  let {testActor, inspector} = env;
+  let view = selectRuleView(inspector);
+  let highlighters = view.highlighters;
+  let config = {inspector, view, highlighters, testActor, helper};
 
-  await testPolygonIframeMovePoint(testActor, helper);
-
-  await helper.finalize();
+  await testPolygonIframeMovePoint(config);
 });
 
-async function testPolygonIframeMovePoint(testActor, helper) {
-  info("Displaying polygon");
-  await helper.show("#polygon", {mode: "cssClipPath"}, "#frame");
-  let { mouse, highlightedNode } = helper;
+async function testPolygonIframeMovePoint(config) {
+  const { inspector, view, testActor, helper } = config;
+  const selector = "#polygon";
+  const property = "clip-path";
 
+  info(`Turn on shapes highlighter for ${selector}`);
+  // Get a reference to the highlighter's target node inside the iframe.
+  let highlightedNode = await getNodeFrontInFrame(selector, "#frame", inspector);
+  // Select the nested node so toggling of the shapes highlighter works from the rule view
+  await selectNode(highlightedNode, inspector);
+  await toggleShapesHighlighter(view, selector, property, true);
+  let { mouse } = helper;
+
+  let onRuleViewChanged = view.once("ruleview-changed");
   info("Moving polygon point visible in iframe");
   await mouse.down(10, 10);
   await mouse.move(20, 20);
   await mouse.up();
   await testActor.reflow();
+  await onRuleViewChanged;
 
-  let computedStyle = await highlightedNode.getComputedStyle();
+  let computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   let definition = computedStyle["clip-path"].value;
   ok(definition.includes("10px 10px"), "Point moved to 10px 10px");
 
+  onRuleViewChanged = view.once("ruleview-changed");
   info("Moving polygon point not visible in iframe");
   await mouse.down(110, 410);
   await mouse.move(120, 420);
   await mouse.up();
   await testActor.reflow();
+  await onRuleViewChanged;
 
-  computedStyle = await highlightedNode.getComputedStyle();
+  computedStyle = await inspector.pageStyle.getComputed(highlightedNode);
   definition = computedStyle["clip-path"].value;
   ok(definition.includes("110px 51.25%"), "Point moved to 110px 51.25%");
 }
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -790,36 +790,44 @@ async function getDisplayedNodeTextConte
 }
 
 /**
  * Toggle the shapes highlighter by simulating a click on the toggle
  * in the rules view with the given selector and property
  *
  * @param {CssRuleView} view
  *        The instance of the rule-view panel
- * @param {Object} highlighters
- *        The highlighters instance of the rule-view panel
  * @param {String} selector
  *        The selector in the rule-view to look for the property in
  * @param {String} property
  *        The name of the property
  * @param {Boolean} show
  *        If true, the shapes highlighter is being shown. If false, it is being hidden
+ * @param {Options} options
+ *        Config option for the shapes highlighter. Contains:
+ *        - {Boolean} transformMode: wether to show the highlighter in transforms mode
  */
-async function toggleShapesHighlighter(view, highlighters, selector, property, show) {
-  info("Toggle shapes highlighter");
-  let container = getRuleViewProperty(view, selector, property).valueSpan;
-  let shapesToggle = container.querySelector(".ruleview-shapeswatch");
+async function toggleShapesHighlighter(view, selector, property, show, options = {}) {
+  info(`Toggle shapes highlighter ${show ? "on" : "off"} for ${property} on ${selector}`);
+  const highlighters = view.highlighters;
+  const container = getRuleViewProperty(view, selector, property).valueSpan;
+  const shapesToggle = container.querySelector(".ruleview-shapeswatch");
+
+  let metaKey = options.transformMode;
+  let ctrlKey = options.transformMode;
+
   if (show) {
     let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
-    shapesToggle.click();
+    EventUtils.sendMouseEvent({type: "click", metaKey, ctrlKey },
+      shapesToggle, view.styleWindow);
     await onHighlighterShown;
   } else {
     let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
-    shapesToggle.click();
+    EventUtils.sendMouseEvent({type: "click", metaKey, ctrlKey },
+      shapesToggle, view.styleWindow);
     await onHighlighterHidden;
   }
 }
 
 /**
  * Expand the provided markup container programatically and  wait for all children to
  * update.
  */
--- a/devtools/client/menus.js
+++ b/devtools/client/menus.js
@@ -143,12 +143,12 @@ exports.menuitems = [
   },
   { separator: true,
     id: "devToolsEndSeparator"
   },
   { id: "getMoreDevtools",
     l10nKey: "getMoreDevtoolsCmd",
     oncommand(event) {
       let window = event.target.ownerDocument.defaultView;
-      window.openUILinkIn("https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/", "tab");
+      window.openTrustedLinkIn("https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/", "tab");
     }
   },
 ];
--- a/devtools/client/netmonitor/initializer.js
+++ b/devtools/client/netmonitor/initializer.js
@@ -60,17 +60,17 @@ window.Netmonitor = {
       toolbox,
       panel,
     };
 
     const openLink = (link) => {
       let parentDoc = toolbox.doc;
       let iframe = parentDoc.getElementById("toolbox-panel-iframe-netmonitor");
       let top = iframe.ownerDocument.defaultView.top;
-      top.openUILinkIn(link, "tab");
+      top.openWebLinkIn(link, "tab");
     };
 
     const openSplitConsole = (err) => {
       toolbox.openSplitConsole().then(() => {
         toolbox.target.logErrorInPage(err, "har");
       });
     };
 
--- a/devtools/client/netmonitor/src/components/MdnLink.js
+++ b/devtools/client/netmonitor/src/components/MdnLink.js
@@ -32,16 +32,16 @@ MDNLink.propTypes = {
 
 function onLearnMoreClick(e, url) {
   e.stopPropagation();
   e.preventDefault();
 
   let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
   let { button, ctrlKey, metaKey } = e;
   let isOSX = Services.appinfo.OS == "Darwin";
+  let where = "tab";
   if (button === 1 || (button === 0 && (isOSX ? metaKey : ctrlKey))) {
-    win.openUILinkIn(url, "tabshifted");
-  } else {
-    win.openUILinkIn(url, "tab");
+    where = "tabshifted";
   }
+  win.openWebLinkIn(url, where, {triggeringPrincipal: win.document.nodePrincipal});
 }
 
 module.exports = MDNLink;
--- a/devtools/client/netmonitor/src/utils/open-request-in-tab.js
+++ b/devtools/client/netmonitor/src/utils/open-request-in-tab.js
@@ -8,17 +8,17 @@ const Services = require("Services");
 const { gDevTools } = require("devtools/client/framework/devtools");
 
 /**
  * Opens given request in a new tab.
  */
 function openRequestInTab(url, requestPostData) {
   let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
   if (!requestPostData) {
-    win.openUILinkIn(url, "tab", {relatedToCurrent: true});
+    win.openWebLinkIn(url, "tab", {relatedToCurrent: true});
   } else {
     openPostRequestInTabHelper({
       url,
       data: requestPostData.postData
     });
   }
 }
 
--- a/devtools/client/scratchpad/scratchpad.js
+++ b/devtools/client/scratchpad/scratchpad.js
@@ -1977,17 +1977,17 @@ var Scratchpad = {
     }
   },
 
   /**
    * Opens the MDN documentation page for Scratchpad.
    */
   openDocumentationPage: function SP_openDocumentationPage() {
     let url = this.strings.GetStringFromName("help.openDocumentationPage");
-    this.browserWindow.openUILinkIn(url, "tab");
+    this.browserWindow.openTrustedLinkIn(url, "tab");
     this.browserWindow.focus();
   },
 };
 
 /**
  * Represents the DebuggerClient connection to a specific tab as used by the
  * Scratchpad.
  *
--- a/devtools/client/shared/AppCacheUtils.jsm
+++ b/devtools/client/shared/AppCacheUtils.jsm
@@ -284,17 +284,17 @@ AppCacheUtils.prototype = {
       throw new Error(l10n.GetStringFromName("noResults"));
     }
     return entries;
   },
 
   viewEntry: function ACU_viewEntry(key) {
     let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
     let url = "about:cache-entry?storage=appcache&context=&eid=&uri=" + key;
-    win.openUILinkIn(url, "tab");
+    win.openTrustedLinkIn(url, "tab");
   },
 
   clearAll: function ACU_clearAll() {
     if (!Services.prefs.getBoolPref("browser.cache.disk.enable")) {
       throw new Error(l10n.GetStringFromName("cacheDisabled"));
     }
 
     let appCacheStorage = Services.cache2.appCacheStorage(Services.loadContextInfo.default, null);
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/ShapesInContextEditor.js
@@ -0,0 +1,298 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+const { debounce } = require("devtools/shared/debounce");
+
+/**
+ * The ShapesInContextEditor:
+ * - communicates with the ShapesHighlighter actor from the server;
+ * - listens to events for shape change and hover point coming from the shape-highlighter;
+ * - writes shape value changes to the CSS declaration it was triggered from;
+ * - synchronises highlighting coordinate points on mouse over between the shapes
+ *   highlighter and the shape value shown in the Rule view.
+ *
+ * It is instantiated once in HighlightersOverlay by calls to .getInContextEditor().
+ */
+class ShapesInContextEditor {
+  constructor(highlighter, inspector, state) {
+    EventEmitter.decorate(this);
+
+    this.inspector = inspector;
+    this.highlighter = highlighter;
+    // Refence to the NodeFront currently being highlighted.
+    this.highlighterTargetNode = null;
+    this.highligherEventHandlers = {};
+    this.highligherEventHandlers["shape-change"] = this.onShapeChange;
+    this.highligherEventHandlers["shape-hover-on"] = this.onShapeHover;
+    this.highligherEventHandlers["shape-hover-off"] = this.onShapeHover;
+    // Mode for shapes highlighter: shape-outside or clip-path. Used to discern
+    // when toggling the highlighter on the same node for different CSS properties.
+    this.mode = null;
+    // Reference to Rule view used to listen for changes
+    this.ruleView = this.inspector.getPanel("ruleview").view;
+    // Reference of |state| from HighlightersOverlay.
+    this.state = state;
+    // Reference to DOM node of the toggle icon for shapes highlighter.
+    this.swatch = null;
+    // Reference to TextProperty where shape changes will be written.
+    this.textProperty = null;
+
+    // Commit triggers expensive DOM changes in TextPropertyEditor.update()
+    // so we debounce it.
+    this.commit = debounce(this.commit, 200, this);
+    this.onChangesApplied = this.onChangesApplied.bind(this);
+    this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
+    this.onNodeFrontChanged = this.onNodeFrontChanged.bind(this);
+    this.onRuleViewChanged = this.onRuleViewChanged.bind(this);
+
+    this.highlighter.on("highlighter-event", this.onHighlighterEvent);
+    this.ruleView.on("ruleview-changed", this.onRuleViewChanged);
+  }
+
+  /**
+  * Called when the element style changes from the Rule view.
+  * If the TextProperty we're acting on isn't enabled anymore or overridden,
+  * turn off the shapes highlighter.
+  */
+  async onRuleViewChanged() {
+    if (this.textProperty &&
+      (!this.textProperty.enabled || this.textProperty.overridden)) {
+      await this.hide();
+    }
+  }
+
+  /**
+   * Toggle the shapes highlighter for the given element.
+   *
+   * @param {NodeFront} node
+   *        The NodeFront of the element with a shape to highlight.
+   * @param {Object} options
+   *        Object used for passing options to the shapes highlighter.
+   */
+  async toggle(node, options, prop) {
+    // Same target node, same mode -> hide and exit OR switch to toggle transform mode.
+    if ((node == this.highlighterTargetNode) && (this.mode === options.mode)) {
+      if (!options.transformMode) {
+        await this.hide();
+        return;
+      }
+
+      options.transformMode = !this.state.shapes.options.transformMode;
+    }
+
+    // Same target node, dfferent modes -> toggle between shape-outside and clip-path.
+    // Hide highlighter for previous property, but continue and show for other property.
+    if ((node == this.highlighterTargetNode) && (this.mode !== options.mode)) {
+      await this.hide();
+    }
+
+    this.textProperty = prop;
+    this.findSwatch();
+    await this.show(node, options);
+  }
+
+  /**
+   * Show the shapes highlighter for the given element.
+   *
+   * @param {NodeFront} node
+   *        The NodeFront of the element with a shape to highlight.
+   * @param {Object} options
+   *        Object used for passing options to the shapes highlighter.
+   */
+  async show(node, options) {
+    let isShown = await this.highlighter.show(node, options);
+    if (!isShown) {
+      return;
+    }
+
+    this.inspector.selection.on("detached-front", this.onNodeFrontChanged);
+    this.inspector.selection.on("new-node-front", this.onNodeFrontChanged);
+    this.highlighterTargetNode = node;
+    this.mode = options.mode;
+    this.emit("show", { node, options });
+  }
+
+  /**
+   * Hide the shapes highlighter.
+   */
+  async hide() {
+    try {
+      await this.highlighter.hide();
+    } catch (err) {
+      // silent error
+    }
+
+    if (this.swatch) {
+      this.swatch.classList.remove("active");
+    }
+    this.swatch = null;
+    this.textProperty = null;
+
+    this.emit("hide", { node: this.highlighterTargetNode });
+    this.inspector.selection.off("detached-front", this.onNodeFrontChanged);
+    this.inspector.selection.off("new-node-front", this.onNodeFrontChanged);
+    this.highlighterTargetNode = null;
+  }
+
+  /**
+   * Identify the swatch (aka toggle icon) DOM node from the TextPropertyEditor of the
+   * TextProperty we're working with. Whenever the TextPropertyEditor is updated (i.e.
+   * when committing the shape value to the Rule view), it rebuilds its DOM and the old
+   * swatch reference becomes invalid. Call this method to identify the current swatch.
+   */
+  findSwatch() {
+    const valueSpan = this.textProperty.editor.valueSpan;
+    this.swatch = valueSpan.querySelector(".ruleview-shapeswatch");
+    this.swatch.classList.add("active");
+  }
+
+  /**
+   * Handle events emitted by the highlighter.
+   * Find any callback assigned to the event type and call it with the given data object.
+   *
+   * @param {Object} data
+   *        The data object sent in the event.
+   */
+  onHighlighterEvent(data) {
+    const handler = this.highligherEventHandlers[data.type];
+    if (!handler || typeof handler !== "function") {
+      return;
+    }
+    handler.call(this, data);
+    this.inspector.highlighters.emit("highlighter-event-handled");
+  }
+
+  /**
+  * Clean up when node selection changes because Rule view and TextPropertyEditor
+  * instances are not automatically destroyed when selection changes.
+  */
+  async onNodeFrontChanged() {
+    try {
+      await this.hide();
+    } catch (err) {
+      // Silent error.
+    }
+  }
+
+  /**
+  * Handler for "shape-change" event from the shapes highlighter.
+  *
+  * @param  {Object} data
+  *         Data associated with the "shape-change" event.
+  *         Contains:
+  *         - {String} value: the new shape value.
+  *         - {String} type: the event type ("shape-change").
+  */
+  onShapeChange(data) {
+    this.preview(data.value);
+    this.commit(data.value);
+  }
+
+  /**
+  * Handler for "shape-hover-on" and "shape-hover-off" events from the shapes highlighter.
+  * Called when the mouse moves over or off of a coordinate point inside the shapes
+  * highlighter. Marks/unmarks the corresponding coordinate node in the shape value
+  * from the Rule view.
+  *
+  * @param  {Object} data
+  *         Data associated with the "shape-hover" event.
+  *         Contains:
+  *         - {String|null} point: coordinate to highlight or null if nothing to highlight
+  *         - {String} type: the event type ("shape-hover-on" or "shape-hover-on").
+  */
+  onShapeHover(data) {
+    if (!this.textProperty) {
+      return;
+    }
+
+    let shapeValueEl = this.swatch.nextSibling;
+    if (!shapeValueEl) {
+      return;
+    }
+    let pointSelector = ".ruleview-shape-point";
+    // First, unmark all highlighted coordinate nodes from Rule view
+    for (let node of shapeValueEl.querySelectorAll(`${pointSelector}.active`)) {
+      node.classList.remove("active");
+    }
+
+    // Exit if there's no coordinate to highlight.
+    if (typeof data.point !== "string") {
+      return;
+    }
+
+    let point = (data.point.includes(",")) ? data.point.split(",")[0] : data.point;
+
+    /**
+    * Build selector for coordinate nodes in shape value that must be highlighted.
+    * Coordinate values for inset() use class names instead of data attributes because
+    * a single node may represent multiple coordinates in shorthand notation.
+    * Example: inset(50px); The node wrapping 50px represents all four inset coordinates.
+    */
+    const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
+    let selector = INSET_POINT_TYPES.includes(point) ?
+                  `${pointSelector}.${point}` :
+                  `${pointSelector}[data-point='${point}']`;
+
+    for (let node of shapeValueEl.querySelectorAll(selector)) {
+      node.classList.add("active");
+    }
+  }
+
+  /**
+  * Preview a shape value on the element without committing the changes to the Rule view.
+  *
+  * @param {String} value
+  *        The shape value to set the current property to
+  */
+  preview(value) {
+    if (!this.textProperty) {
+      return;
+    }
+    // Update the element's style to see live results.
+    this.textProperty.rule.previewPropertyValue(this.textProperty, value);
+    // Update the text of CSS value in the Rule view. This makes it inert.
+    // When commit() is called, the value is reparsed and its DOM structure rebuilt.
+    this.swatch.nextSibling.textContent = value;
+  }
+
+  /**
+  * Commit a shape value change which triggers an expensive operation that rebuilds
+  * part of the DOM of the TextPropertyEditor. Called in a debounced manner; see
+  * constructor.
+  *
+  * @param {String} value
+  *        The shape value for the current property
+  */
+  commit(value) {
+    if (!this.textProperty) {
+      return;
+    }
+    this.ruleView.once("ruleview-changed", this.onChangesApplied);
+    this.textProperty.setValue(value);
+  }
+
+  /**
+  * Handler for "ruleview-changed" event triggered by the Rule view.
+  * Called once after the shape value has been written to the element's style and Rule
+  * view updated. Triggers an event on the HighlightersOverlay that is expected by
+  * tests in order to check if the shape value has been correctly applied.
+  */
+  onChangesApplied() {
+    // When TextPropertyEditor updates it thrashes the previous swatch DOM node. Find and
+    // store the new swatch node.
+    this.findSwatch();
+    this.inspector.highlighters.emit("shapes-highlighter-changes-applied");
+  }
+
+  destroy() {
+    this.highlighter.off("highlighter-event", this.onHighlighterEvent);
+    this.ruleView.off("ruleview-changed", this.onRuleViewChanged);
+    this.highligherEventHandlers = {};
+  }
+}
+
+module.exports = ShapesInContextEditor;
--- a/devtools/client/shared/widgets/moz.build
+++ b/devtools/client/shared/widgets/moz.build
@@ -17,16 +17,17 @@ DevToolsModules(
     'CubicBezierWidget.js',
     'FastListWidget.js',
     'FilterWidget.js',
     'FlameGraph.js',
     'Graphs.js',
     'GraphsWorker.js',
     'LineGraphWidget.js',
     'MountainGraphWidget.js',
+    'ShapesInContextEditor.js',
     'SideMenuWidget.jsm',
     'SimpleListWidget.jsm',
     'Spectrum.js',
     'TableWidget.js',
     'TreeWidget.js',
     'VariablesView.jsm',
     'VariablesViewController.jsm',
     'view-helpers.js',
--- a/devtools/client/styleeditor/StyleEditorUI.jsm
+++ b/devtools/client/styleeditor/StyleEditorUI.jsm
@@ -483,17 +483,17 @@ StyleEditorUI.prototype = {
     }
   },
 
   /**
    * Open a particular stylesheet in a new tab.
    */
   _openLinkNewTab: function() {
     if (this._contextMenuStyleSheet) {
-      this._window.openUILinkIn(this._contextMenuStyleSheet.href, "tab");
+      this._window.openWebLinkIn(this._contextMenuStyleSheet.href, "tab");
     }
   },
 
   /**
    * 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,21 @@ add_task(async function() {
     "The menu item is not disabled");
   is(ui._openLinkNewTabItem.getAttribute("hidden"), "false",
     "The menu item is not hidden");
 
   let url = "https://example.com/browser/devtools/client/styleeditor/test/" +
     "simple.css";
   is(ui._contextMenuStyleSheet.href, url, "Correct URL for sheet");
 
-  let originalOpenUILinkIn = ui._window.openUILinkIn;
+  let originalOpenWebLinkIn = ui._window.openWebLinkIn;
   let tabOpenedDefer = new Promise(resolve => {
-    ui._window.openUILinkIn = newUrl => {
-      // Reset the actual openUILinkIn function before proceeding.
-      ui._window.openUILinkIn = originalOpenUILinkIn;
+    ui._window.openWebLinkIn = newUrl => {
+      // Reset the actual openWebLinkIn function before proceeding.
+      ui._window.openWebLinkIn = originalOpenWebLinkIn;
 
       is(newUrl, url, "The correct tab has been opened");
       resolve();
     };
   });
 
   ui._openLinkNewTabItem.click();
 
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -412,17 +412,18 @@
   background-size: 1em;
 }
 
 .ruleview-grid {
   background: url("chrome://devtools/skin/images/grid.svg");
   border-radius: 0;
 }
 
-.ruleview-shape-point.active {
+.ruleview-shape-point.active,
+.ruleview-shapeswatch.active + .ruleview-shape > .ruleview-shape-point:hover {
   background-color: var(--rule-highlight-background-color);
 }
 
 .ruleview-colorswatch::before {
   content: '';
   background-color: #eee;
   background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
                     linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
--- a/devtools/client/webconsole/hudservice.js
+++ b/devtools/client/webconsole/hudservice.js
@@ -376,21 +376,21 @@ 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) {
     let isOSX = Services.appinfo.OS == "Darwin";
+    let where = "tab";
     if (e && (e.button === 1 || (e.button === 0 && (isOSX ? e.metaKey : e.ctrlKey)))) {
-      this.chromeUtilsWindow.openUILinkIn(link, "tabshifted");
-    } else {
-      this.chromeUtilsWindow.openUILinkIn(link, "tab");
+      where = "tabshifted";
     }
+    this.chromeUtilsWindow.openWebLinkIn(link, where);
   },
 
   /**
    * Open a link in Firefox's view source.
    *
    * @param string sourceURL
    *        The URL of the file.
    * @param integer sourceLine
--- a/devtools/client/webconsole/old/test/browser_webconsole_open-links-without-callback.js
+++ b/devtools/client/webconsole/old/test/browser_webconsole_open-links-without-callback.js
@@ -32,23 +32,23 @@ function test() {
     let messageNode = messages[0].matched.values().next().value;
 
     // The correct anchor is second in the message node; the first anchor has
     // class .cm-variable. Ignore the first one by not matching anchors that
     // have the class .cm-variable.
     let urlNode = messageNode.querySelector("a:not(.cm-variable)");
 
     let linkOpened = false;
-    let oldOpenUILinkIn = window.openUILinkIn;
-    window.openUILinkIn = function (aLink) {
+    let oldOpenWebLinkIn = window.openWebLinkIn;
+    window.openWebLinkIn = function (aLink) {
       if (aLink == TEST_PAGE_URI) {
         linkOpened = true;
       }
     };
 
     EventUtils.synthesizeMouseAtCenter(urlNode, {}, hud.iframeWindow);
 
     ok(linkOpened, "Clicking the URL opens the desired page");
-    window.openUILinkIn = oldOpenUILinkIn;
+    window.openWebLinkIn = oldOpenWebLinkIn;
   }
 
   Task.spawn(runner).then(finishTest);
 }
--- a/devtools/client/webconsole/old/test/head.js
+++ b/devtools/client/webconsole/old/test/head.js
@@ -1799,21 +1799,23 @@ function getSourceActor(sources, URL) {
  * Verify that clicking on a link from a popup notification message tries to
  * open the expected URL.
  */
 function simulateMessageLinkClick(element, expectedLink) {
   let deferred = defer();
 
   // Invoke the click event and check if a new tab would
   // open to the correct page.
-  let oldOpenUILinkIn = window.openUILinkIn;
-  window.openUILinkIn = function (link) {
+  let oldOpenWebLinkIn = window.openWebLinkIn;
+  let oldOpenTrustedLinkIn = window.openTrustedLinkIn;
+  window.openTrustedLinkIn = window.openWebLinkIn = function (link) {
     if (link == expectedLink) {
       ok(true, "Clicking the message link opens the desired page");
-      window.openUILinkIn = oldOpenUILinkIn;
+      window.openWebLinkIn = oldOpenWebLinkIn;
+      window.openTrustedLinkIn = oldOpenTrustedLinkIn;
       deferred.resolve();
     }
   };
 
   let event = new MouseEvent("click", {
     detail: 1,
     button: 0,
     bubbles: true,
--- a/devtools/client/webconsole/test/mochitest/head.js
+++ b/devtools/client/webconsole/test/mochitest/head.js
@@ -492,39 +492,42 @@ 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) {
-  // Override openUILinkIn to prevent navigating.
-  let oldOpenUILinkIn = window.openUILinkIn;
+  // Override LinkIn methods to prevent navigating.
+  let oldOpenTrustedLinkIn = window.openTrustedLinkIn;
+  let oldOpenWebLinkIn = window.openWebLinkIn;
 
   const onOpenLink = new Promise((resolve) => {
-    window.openUILinkIn = function(link, where) {
-      window.openUILinkIn = oldOpenUILinkIn;
+    window.openWebLinkIn = window.openTrustedLinkIn = function(link, where) {
+      window.openTrustedLinkIn = oldOpenTrustedLinkIn;
+      window.openWebLinkIn = oldOpenWebLinkIn;
       resolve({link: link, where});
     };
-
     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 openUILinkIn was not called.
+  // 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.openUILinkIn = oldOpenUILinkIn;
+      window.openTrustedLinkIn = oldOpenTrustedLinkIn;
+      window.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
@@ -85,17 +85,19 @@ function createContextMenu(jsterm, paren
     label: l10n.getStr("webconsole.menu.openURL.label"),
     accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"),
     visible: source === MESSAGE_SOURCE.NETWORK,
     click: () => {
       if (!request) {
         return;
       }
       let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-      mainWindow.openUILinkIn(request.url, "tab");
+      mainWindow.openWebLinkIn(request.url, "tab", {
+        triggeringPrincipal: mainWindow.document.nodePrincipal,
+      });
     },
   }));
 
   // 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
@@ -177,17 +177,17 @@ var UI = {
     }
     this._updatePromise = promise.resolve();
   },
 
   openInBrowser: function (url) {
     // Open a URL in a Firefox window
     let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
     if (mainWindow) {
-      mainWindow.openUILinkIn(url, "tab");
+      mainWindow.openWebLinkIn(url, "tab");
       mainWindow.focus()
     } else {
       window.open(url);
     }
   },
 
   updateTitle: function () {
     let project = AppManager.selectedProject;
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -836,20 +836,19 @@ class ShapesHighlighter extends AutoRefr
       let [newX, newY] = apply(this.transformMatrix, vector);
       let precisionX = getDecimalPrecision(unitX);
       let precisionY = getDecimalPrecision(unitY);
       newX = (newX * ratioX).toFixed(precisionX);
       newY = (newY * ratioY).toFixed(precisionY);
 
       return `${newX}${unitX} ${newY}${unitY}`;
     }).join(", ");
-    polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
-                                      `polygon(${polygonDef})`;
+    polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();
 
-    this.currentNode.style.setProperty(this.property, polygonDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
   }
 
   /**
    * Transform a circle depending on the current transformation matrix.
    * @param {Number} transX the number of pixels the shape is translated on the x axis
    *                 before scaling
    */
   _transformCircle(transX = null) {
@@ -860,21 +859,19 @@ class ShapesHighlighter extends AutoRefr
     let [newCx, newCy] = apply(this.transformMatrix, [valueX / ratioX, valueY / ratioY]);
     if (transX !== null) {
       // As part of scaling, the shape is translated to be tangent to the line y=0.
       // To get the new radius, we translate the new cx back to that point and get
       // the distance to the line y=0.
       radius = `${Math.abs((newCx - transX) * ratioRad)}${unitRad}`;
     }
 
-    let circleDef = (this.geometryBox) ?
-      `circle(${radius} at ${newCx * ratioX}${unitX} ` +
-        `${newCy * ratioY}${unitY} ${this.geometryBox}` :
-      `circle(${radius} at ${newCx * ratioX}${unitX} ${newCy * ratioY}${unitY}`;
-    this.currentNode.style.setProperty(this.property, circleDef, "important");
+    let circleDef = `circle(${radius} at ${newCx * ratioX}${unitX} ` +
+        `${newCy * ratioY}${unitY}) ${this.geometryBox}`.trim();
+    this.emit("highlighter-event", { type: "shape-change", value: circleDef });
   }
 
   /**
    * Transform an ellipse depending on the current transformation matrix.
    * @param {Number} transX the number of pixels the shape is translated on the x axis
    *                 before scaling
    * @param {Number} transY the number of pixels the shape is translated on the y axis
    *                 before scaling
@@ -888,22 +885,19 @@ class ShapesHighlighter extends AutoRefr
     if (transX !== null && transY !== null) {
       // As part of scaling, the shape is translated to be tangent to the lines y=0 & x=0.
       // To get the new radii, we translate the new center back to that point and get the
       // distances to the line x=0 and y=0.
       rx = `${Math.abs((newCx - transX) * ratioRX)}${unitRX}`;
       ry = `${Math.abs((newCy - transY) * ratioRY)}${unitRY}`;
     }
 
-    let ellipseDef = (this.geometryBox) ?
-        `ellipse(${rx} ${ry} at ${newCx * ratioX}${unitX} ` +
-          `${newCy * ratioY}${unitY}) ${this.geometryBox}` :
-        `ellipse(${rx} ${ry} at ${newCx * ratioX}${unitX} ` +
-          `${newCy * ratioY}${unitY})`;
-    this.currentNode.style.setProperty(this.property, ellipseDef, "important");
+    let ellipseDef = `ellipse(${rx} ${ry} at ${newCx * ratioX}${unitX} ` +
+          `${newCy * ratioY}${unitY}) ${this.geometryBox}`.trim();
+    this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
   }
 
   /**
    * Transform an inset depending on the current transformation matrix.
    */
   _transformInset() {
     let { top, left, right, bottom } = this[_dragging].pointsInfo;
     let { width, height } = this.currentDimensions;
@@ -923,17 +917,17 @@ class ShapesHighlighter extends AutoRefr
     newBottom = `${(height - newBottom) * bottom.ratio}${bottom.unit}`;
 
     let round = this.insetRound;
     let insetDef = (round) ?
           `inset(${newTop} ${newRight} ${newBottom} ${newLeft} round ${round})` :
           `inset(${newTop} ${newRight} ${newBottom} ${newLeft})`;
     insetDef += (this.geometryBox) ? this.geometryBox : "";
 
-    this.currentNode.style.setProperty(this.property, insetDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: insetDef });
   }
 
   /**
    * Handle a click when highlighting a polygon.
    * @param {Number} pageX the x coordinate of the click
    * @param {Number} pageY the y coordinate of the click
    */
   _handlePolygonClick(pageX, pageY) {
@@ -956,18 +950,18 @@ class ShapesHighlighter extends AutoRefr
     let ratioY = (valueY / yComputed) || 1;
 
     this.setCursor("grabbing");
     this[_dragging] = { point, unitX, unitY, valueX, valueY,
                         ratioX, ratioY, x: pageX, y: pageY };
   }
 
   /**
-   * Set the inline style of the polygon, replacing the given point with the given x/y
-   * coords.
+   * Update the dragged polygon point with the given x/y coords and update
+   * the element style.
    * @param {Number} pageX the new x coordinate of the point
    * @param {Number} pageY the new y coordinate of the point
    */
   _handlePolygonMove(pageX, pageY) {
     let { point, unitX, unitY, valueX, valueY, ratioX, ratioY, x, y } = this[_dragging];
     let deltaX = (pageX - x) * ratioX;
     let deltaY = (pageY - y) * ratioY;
     let precisionX = getDecimalPrecision(unitX);
@@ -975,62 +969,59 @@ class ShapesHighlighter extends AutoRefr
     let newX = (valueX + deltaX).toFixed(precisionX);
     let newY = (valueY + deltaY).toFixed(precisionY);
 
     let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
     polygonDef += this.coordUnits.map((coords, i) => {
       return (i === point) ?
         `${newX}${unitX} ${newY}${unitY}` : `${coords[0]} ${coords[1]}`;
     }).join(", ");
-    polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
-                                      `polygon(${polygonDef})`;
+    polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();
 
-    this.currentNode.style.setProperty(this.property, polygonDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
   }
 
   /**
-   * Set the inline style of the polygon, adding a new point.
+   * Add new point to the polygon defintion and update element style.
    * TODO: Bug 1436054 - Do not default to percentage unit when inserting new point.
    * https://bugzilla.mozilla.org/show_bug.cgi?id=1436054
    *
    * @param {Number} after the index of the point that the new point should be added after
    * @param {Number} x the x coordinate of the new point
    * @param {Number} y the y coordinate of the new point
    */
   _addPolygonPoint(after, x, y) {
     let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
     polygonDef += this.coordUnits.map((coords, i) => {
       return (i === after) ? `${coords[0]} ${coords[1]}, ${x}% ${y}%` :
                              `${coords[0]} ${coords[1]}`;
     }).join(", ");
-    polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
-                                      `polygon(${polygonDef})`;
+    polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();
 
     this.hoveredPoint = after + 1;
     this._emitHoverEvent(this.hoveredPoint);
-    this.currentNode.style.setProperty(this.property, polygonDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
   }
 
   /**
-   * Set the inline style of the polygon, deleting the given point.
+   * Remove point from polygon defintion and update the element style.
    * @param {Number} point the index of the point to delete
    */
   _deletePolygonPoint(point) {
     let coordinates = this.coordUnits.slice();
     coordinates.splice(point, 1);
     let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
     polygonDef += coordinates.map((coords, i) => {
       return `${coords[0]} ${coords[1]}`;
     }).join(", ");
-    polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
-                                      `polygon(${polygonDef})`;
+    polygonDef = `polygon(${polygonDef}) ${this.geometryBox}`.trim();
 
     this.hoveredPoint = null;
     this._emitHoverEvent(this.hoveredPoint);
-    this.currentNode.style.setProperty(this.property, polygonDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: polygonDef });
   }
   /**
    * Handle a click when highlighting a circle.
    * @param {Number} pageX the x coordinate of the click
    * @param {Number} pageY the y coordinate of the click
    */
   _handleCircleClick(pageX, pageY) {
     let { width, height } = this.currentDimensions;
@@ -1064,53 +1055,50 @@ class ShapesHighlighter extends AutoRefr
       value = (isUnitless(value)) ? radius : parseFloat(value);
       let ratio = (value / radius) || 1;
 
       this[_dragging] = { point, value, origRadius: radius, unit, ratio };
     }
   }
 
   /**
-   * Set the inline style of the circle, setting the center/radius according to the
-   * mouse position.
+   * Set the center/radius of the circle according to the mouse position and
+   * update the element style.
    * @param {String} point either "center" or "radius"
    * @param {Number} pageX the x coordinate of the mouse position, in terms of %
    *        relative to the element
    * @param {Number} pageY the y coordinate of the mouse position, in terms of %
    *        relative to the element
    */
   _handleCircleMove(point, pageX, pageY) {
     let { radius, cx, cy } = this.coordUnits;
 
     if (point === "center") {
       let { unitX, unitY, valueX, valueY, ratioX, ratioY, x, y} = this[_dragging];
       let deltaX = (pageX - x) * ratioX;
       let deltaY = (pageY - y) * ratioY;
       let newCx = `${valueX + deltaX}${unitX}`;
       let newCy = `${valueY + deltaY}${unitY}`;
-      let circleDef = (this.geometryBox) ?
-            `circle(${radius} at ${newCx} ${newCy}) ${this.geometryBox}` :
-            `circle(${radius} at ${newCx} ${newCy})`;
+      // if not defined by the user, geometryBox will be an empty string; trim() cleans up
+      let circleDef = `circle(${radius} at ${newCx} ${newCy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, circleDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: circleDef });
     } else if (point === "radius") {
       let { value, unit, origRadius, ratio } = this[_dragging];
       // convert center point to px, then get distance between center and mouse.
       let { x: pageCx, y: pageCy } = this.convertPercentToPageCoords(this.coordinates.cx,
                                                                      this.coordinates.cy);
       let newRadiusPx = getDistance(pageCx, pageCy, pageX, pageY);
 
       let delta = (newRadiusPx - origRadius) * ratio;
       let newRadius = `${value + delta}${unit}`;
 
-      let circleDef = (this.geometryBox) ?
-                      `circle(${newRadius} at ${cx} ${cy} ${this.geometryBox}` :
-                      `circle(${newRadius} at ${cx} ${cy}`;
+      let circleDef = `circle(${newRadius} at ${cx} ${cy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, circleDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: circleDef });
     }
   }
 
   /**
    * Handle a click when highlighting an ellipse.
    * @param {Number} pageX the x coordinate of the click
    * @param {Number} pageY the y coordinate of the click
    */
@@ -1154,63 +1142,60 @@ class ShapesHighlighter extends AutoRefr
       value = (isUnitless(value)) ? ry : parseFloat(value);
       let ratio = (value / ry) || 1;
 
       this[_dragging] = { point, value, origRadius: ry, unit, ratio };
     }
   }
 
   /**
-   * Set the inline style of the ellipse, setting the center/rx/ry according to the
-   * mouse position.
+   * Set center/rx/ry of the ellispe according to the mouse position and update the
+   * element style.
    * @param {String} point "center", "rx", or "ry"
    * @param {Number} pageX the x coordinate of the mouse position, in terms of %
    *        relative to the element
    * @param {Number} pageY the y coordinate of the mouse position, in terms of %
    *        relative to the element
    */
   _handleEllipseMove(point, pageX, pageY) {
     let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
     let { rx, ry, cx, cy } = this.coordUnits;
 
     if (point === "center") {
       let { unitX, unitY, valueX, valueY, ratioX, ratioY, x, y} = this[_dragging];
       let deltaX = (pageX - x) * ratioX;
       let deltaY = (pageY - y) * ratioY;
       let newCx = `${valueX + deltaX}${unitX}`;
       let newCy = `${valueY + deltaY}${unitY}`;
-      let ellipseDef = (this.geometryBox) ?
-        `ellipse(${rx} ${ry} at ${newCx} ${newCy}) ${this.geometryBox}` :
-        `ellipse(${rx} ${ry} at ${newCx} ${newCy})`;
+      let ellipseDef =
+        `ellipse(${rx} ${ry} at ${newCx} ${newCy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
     } else if (point === "rx") {
       let { value, unit, origRadius, ratio } = this[_dragging];
       let newRadiusPercent = Math.abs(percentX - this.coordinates.cx);
       let { width } = this.currentDimensions;
       let delta = ((newRadiusPercent / 100 * width) - origRadius) * ratio;
       let newRadius = `${value + delta}${unit}`;
 
-      let ellipseDef = (this.geometryBox) ?
-        `ellipse(${newRadius} ${ry} at ${cx} ${cy}) ${this.geometryBox}` :
-        `ellipse(${newRadius} ${ry} at ${cx} ${cy})`;
+      let ellipseDef =
+        `ellipse(${newRadius} ${ry} at ${cx} ${cy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
     } else if (point === "ry") {
       let { value, unit, origRadius, ratio } = this[_dragging];
       let newRadiusPercent = Math.abs(percentY - this.coordinates.cy);
       let { height } = this.currentDimensions;
       let delta = ((newRadiusPercent / 100 * height) - origRadius) * ratio;
       let newRadius = `${value + delta}${unit}`;
 
-      let ellipseDef = (this.geometryBox) ?
-        `ellipse(${rx} ${newRadius} at ${cx} ${cy}) ${this.geometryBox}` :
-        `ellipse(${rx} ${newRadius} at ${cx} ${cy})`;
+      let ellipseDef =
+        `ellipse(${rx} ${newRadius} at ${cx} ${cy}) ${this.geometryBox}`.trim();
 
-      this.currentNode.style.setProperty(this.property, ellipseDef, "important");
+      this.emit("highlighter-event", { type: "shape-change", value: ellipseDef });
     }
   }
 
   /**
    * Handle a click when highlighting an inset.
    * @param {Number} pageX the x coordinate of the click
    * @param {Number} pageY the y coordinate of the click
    */
@@ -1230,18 +1215,18 @@ class ShapesHighlighter extends AutoRefr
     value = (isUnitless(value)) ? computedValue : parseFloat(value);
     let ratio = (value / computedValue) || 1;
     let origValue = (point === "left" || point === "right") ? pageX : pageY;
 
     this[_dragging] = { point, value, origValue, unit, ratio };
   }
 
   /**
-   * Set the inline style of the inset, setting top/left/right/bottom according to the
-   * mouse position.
+   * Set the top/left/right/bottom of the inset shape according to the mouse position
+   * and update the element style.
    * @param {String} point "top", "left", "right", or "bottom"
    * @param {Number} pageX the x coordinate of the mouse position, in terms of %
    *        relative to the element
    * @param {Number} pageY the y coordinate of the mouse position, in terms of %
    *        relative to the element
    * @memberof ShapesHighlighter
    */
   _handleInsetMove(point, pageX, pageY) {
@@ -1263,17 +1248,17 @@ class ShapesHighlighter extends AutoRefr
       bottom = `${value - delta}${unit}`;
     }
     let insetDef = (round) ?
       `inset(${top} ${right} ${bottom} ${left} round ${round})` :
       `inset(${top} ${right} ${bottom} ${left})`;
 
     insetDef += (this.geometryBox) ? this.geometryBox : "";
 
-    this.currentNode.style.setProperty(this.property, insetDef, "important");
+    this.emit("highlighter-event", { type: "shape-change", value: insetDef });
   }
 
   _handleMouseMoveNotDragging(pageX, pageY) {
     let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
     if (this.transformMode) {
       let point = this.getTransformPointAt(percentX, percentY);
       this.hoveredPoint = point;
       this._handleMarkerHover(point);
--- a/devtools/server/actors/utils/shapes-utils.js
+++ b/devtools/server/actors/utils/shapes-utils.js
@@ -4,17 +4,17 @@
  * Get the distance between two points on a plane.
  * @param {Number} x1 the x coord of the first point
  * @param {Number} y1 the y coord of the first point
  * @param {Number} x2 the x coord of the second point
  * @param {Number} y2 the y coord of the second point
  * @returns {Number} the distance between the two points
  */
 const getDistance = (x1, y1, x2, y2) => {
-  return Math.hypot(x2 - x1, y2 - y1);
+  return Math.round(Math.hypot(x2 - x1, y2 - y1));
 };
 
 /**
  * Determine if the given x/y coords are along the edge of the given ellipse.
  * We allow for a small area around the edge that still counts as being on the edge.
  * @param {Number} x the x coordinate of the click
  * @param {Number} y the y coordinate of the click
  * @param {Number} cx the x coordinate of the center of the ellipse
--- a/devtools/shared/gcli/commands/screenshot.js
+++ b/devtools/shared/gcli/commands/screenshot.js
@@ -155,17 +155,19 @@ exports.items = [
       }
 
       // Click handler
       if (imageSummary.href || imageSummary.filename) {
         root.style.cursor = "pointer";
         root.addEventListener("click", () => {
           if (imageSummary.href) {
             let mainWindow = context.environment.chromeWindow;
-            mainWindow.openUILinkIn(imageSummary.href, "tab");
+            mainWindow.openWebLinkIn(imageSummary.href, "tab", {
+              triggeringPrincipal: document.nodePrincipal,
+            });
           } else if (imageSummary.filename) {
             const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
             file.initWithPath(imageSummary.filename);
             file.reveal();
           }
         });
       }
 
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -3403,34 +3403,41 @@ ContentParent::DeallocPNeckoParent(PNeck
   delete necko;
   return true;
 }
 
 PPrintingParent*
 ContentParent::AllocPPrintingParent()
 {
 #ifdef NS_PRINTING
-  MOZ_ASSERT(!mPrintingParent,
-             "Only one PrintingParent should be created per process.");
+  MOZ_RELEASE_ASSERT(!mPrintingParent,
+                     "Only one PrintingParent should be created per process.");
 
   // Create the printing singleton for this process.
   mPrintingParent = new PrintingParent();
+
+  // Take another reference for IPDL code.
+  mPrintingParent.get()->AddRef();
+
   return mPrintingParent.get();
 #else
   MOZ_ASSERT_UNREACHABLE("Should never be created if no printing.");
   return nullptr;
 #endif
 }
 
 bool
 ContentParent::DeallocPPrintingParent(PPrintingParent* printing)
 {
 #ifdef NS_PRINTING
-  MOZ_ASSERT(mPrintingParent == printing,
-             "Only one PrintingParent should have been created per process.");
+  MOZ_RELEASE_ASSERT(mPrintingParent == printing,
+    "Only one PrintingParent should have been created per process.");
+
+  // Release reference taken for IPDL code.
+  static_cast<PrintingParent*>(printing)->Release();
 
   mPrintingParent = nullptr;
 #else
   MOZ_ASSERT_UNREACHABLE("Should never have been created if no printing.");
 #endif
   return true;
 }
 
--- a/dom/security/nsContentSecurityManager.cpp
+++ b/dom/security/nsContentSecurityManager.cpp
@@ -160,16 +160,19 @@ nsContentSecurityManager::AllowInsecureR
 }
 
 /* static */ nsresult
 nsContentSecurityManager::CheckFTPSubresourceLoad(nsIChannel* aChannel)
 {
   // We dissallow using FTP resources as a subresource everywhere.
   // The only valid way to use FTP resources is loading it as
   // a top level document.
+  if (!mozilla::net::nsIOService::BlockFTPSubresources()) {
+    return NS_OK;
+  }
 
   nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
   if (!loadInfo) {
     return NS_OK;
   }
 
   nsContentPolicyType type = loadInfo->GetExternalContentPolicyType();
   if (type == nsIContentPolicy::TYPE_DOCUMENT) {
--- a/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html
+++ b/dom/tests/mochitest/localstorage/test_localStorageQuotaPrivateBrowsing_perwindowpb.html
@@ -163,16 +163,17 @@ function whenDelayedStartupFinished(aCal
 function testOnWindow(aIsPrivate, callback) {
   var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIWebNavigation)
                          .QueryInterface(Ci.nsIDocShellTreeItem)
                          .rootTreeItem
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindow);
 
-  mainWindow.openUILinkIn(CONTENT_PAGE, "window", {private: aIsPrivate});
+  mainWindow.openWebLinkIn(CONTENT_PAGE, "window", {
+                                  private: aIsPrivate });
   whenDelayedStartupFinished(callback);
 };
 </script>
 </head>
 <body onload="startTest();">
 </body>
 </html>
--- a/gfx/2d/DrawEventRecorder.cpp
+++ b/gfx/2d/DrawEventRecorder.cpp
@@ -92,20 +92,20 @@ void
 DrawEventRecorderMemory::Flush()
 {
 }
 
 void
 DrawEventRecorderMemory::FlushItem(IntRect aRect)
 {
   MOZ_RELEASE_ASSERT(!aRect.IsEmpty());
-  // Detatching our existing resources will add some
+  // Detaching our existing resources will add some
   // destruction events to our stream so we need to do that
   // first.
-  DetatchResources();
+  DetachResources();
 
   // See moz2d_renderer.rs for a description of the stream format
   WriteElement(mIndex, mOutputStream.mLength);
 
   // write out the fonts into the extra data section
   mSerializeCallback(mOutputStream, mUnscaledFonts);
   WriteElement(mIndex, mOutputStream.mLength);
 
--- a/gfx/2d/DrawEventRecorder.h
+++ b/gfx/2d/DrawEventRecorder.h
@@ -25,17 +25,17 @@ class DrawEventRecorderPrivate : public 
 {
 public:
   MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(DrawEventRecorderPrivate, override)
 
   DrawEventRecorderPrivate();
   virtual ~DrawEventRecorderPrivate() { }
   virtual bool Finish() override { ClearResources(); return true; }
   virtual void FlushItem(IntRect) { }
-  void DetatchResources() {
+  void DetachResources() {
     // The iteration is a bit awkward here because our iterator will
     // be invalidated by the removal
     for (auto font = mStoredFonts.begin(); font != mStoredFonts.end(); ) {
       auto oldFont = font++;
       (*oldFont)->RemoveUserData(reinterpret_cast<UserDataKey*>(this));
     }
     for (auto surface = mStoredSurfaces.begin(); surface != mStoredSurfaces.end(); ) {
       auto oldSurface = surface++;
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/ion/bug1452581.js
@@ -0,0 +1,16 @@
+// |jit-test| error:7
+var g = newGlobal()
+g.parent = this
+g.eval("(" + function() {
+    Debugger(parent).onExceptionUnwind = function(frame) {
+        frame.older
+    }
+} + ")()")
+function f1(i) {
+    return f2(i|0);
+};
+function f2(i) {
+    if (i === 0) throw 7;
+    return f1(i - 1);
+}
+f1(10);
--- a/js/src/jit-test/tests/wasm/globals.js
+++ b/js/src/jit-test/tests/wasm/globals.js
@@ -1,9 +1,9 @@
-const { Instance, Module } = WebAssembly;
+const { Instance, Module, LinkError } = WebAssembly;
 
 // Locally-defined globals
 assertErrorMessage(() => wasmEvalText(`(module (global))`), SyntaxError, /parsing/);
 assertErrorMessage(() => wasmEvalText(`(module (global i32))`), SyntaxError, /parsing/);
 assertErrorMessage(() => wasmEvalText(`(module (global (mut i32)))`), SyntaxError, /parsing/);
 
 // Initializer expressions.
 wasmFailValidateText(`(module (global i32 (f32.const 13.37)))`, /type mismatch/);
@@ -126,35 +126,35 @@ assertEq(module.getter(), 42);
 // Adapt to ongoing experiment with WebAssembly.Global.
 // assertEq() will not trigger @@toPrimitive, so we must have a cast here.
 if (typeof WebAssembly.Global === "function")
     assertEq(Number(module.value), 42);
 else
     assertEq(module.value, 42);
 
 // Can only import numbers (no implicit coercions).
-module = new WebAssembly.Module(wasmTextToBinary(`(module
+module = new Module(wasmTextToBinary(`(module
     (global (import "globs" "i32") i32)
     (global (import "globs" "f32") f32)
     (global (import "globs" "f64") f32)
 )`));
 
 const assertLinkFails = (m, imp, err) => {
-    assertErrorMessage(() => new WebAssembly.Instance(m, imp), WebAssembly.LinkError, err);
+    assertErrorMessage(() => new Instance(m, imp), LinkError, err);
 }
 
 var imp = {
     globs: {
         i32: 0,
         f32: Infinity,
         f64: NaN
     }
 };
 
-let i = new WebAssembly.Instance(module, imp);
+let i = new Instance(module, imp);
 
 for (let v of [
     null,
     {},
     "42",
     /not a number/,
     false,
     undefined,
@@ -247,61 +247,69 @@ function testInitExpr(type, initialValue
 
 testInitExpr('i32', 13, 37, x => x|0);
 testInitExpr('f32', 13.37, 0.1989, Math.fround);
 testInitExpr('f64', 13.37, 0.1989, x => +x);
 
 // Int64.
 
 // Import and export
+
+// The test for a Number value dominates the guard against int64.
+assertErrorMessage(() => wasmEvalText(`(module
+                                        (import "globals" "x" (global i64)))`,
+                                      {globals: {x:false}}),
+                   LinkError,
+                   /import object field 'x' is not a Number/);
+
+// The imported value is a Number, so the int64 guard should stop us
+assertErrorMessage(() => wasmEvalText(`(module
+                                        (import "globals" "x" (global i64)))`,
+                                      {globals: {x:42}}),
+                   LinkError,
+                   /cannot pass i64 to or from JS/);
+
 if (typeof WebAssembly.Global === "undefined") {
 
-    // Without WebAssembly.Global, i64 cannot be imported or exported
+    // Cannot export int64 at all.
 
-    module = new WebAssembly.Module(wasmTextToBinary(`(module (import "globals" "x" (global i64)))`));
-    assertErrorMessage(() => new WebAssembly.Instance(module, {globals: {x:42}}),
-                       WebAssembly.LinkError,
+    assertErrorMessage(() => wasmEvalText(`(module
+                                            (global i64 (i64.const 42))
+                                            (export "" global 0))`),
+                       LinkError,
                        /cannot pass i64 to or from JS/);
 
-    module = new WebAssembly.Module(wasmTextToBinary(`(module (global i64 (i64.const 42)) (export "" global 0))`));
-    assertErrorMessage(() => new WebAssembly.Instance(module), WebAssembly.LinkError, /cannot pass i64 to or from JS/);
-
-}
-else {
+} else {
 
     // We can import and export i64 globals as cells.  They cannot be created
     // from JS because there's no way to specify a non-zero initial value; that
     // restriction is tested later.  But we can export one from a module and
     // import it into another.
 
-    let i = new WebAssembly.Instance(
-        new WebAssembly.Module(
-            wasmTextToBinary(`(module
-                (global (export "g") i64 (i64.const 37))
-                (global (export "h") (mut i64) (i64.const 37)))`)));
+    let i = wasmEvalText(`(module
+                           (global (export "g") i64 (i64.const 37))
+                           (global (export "h") (mut i64) (i64.const 37)))`);
 
-    let j = new WebAssembly.Instance(
-        new WebAssembly.Module(
-            wasmTextToBinary(`(module
-                (import "globals" "g" (global i64))
-                (func (export "f") (result i32)
-                    (i64.eq (get_global 0) (i64.const 37))))`)),
-        {globals: {g: i.exports.g}});
+    let j = wasmEvalText(`(module
+                           (import "globals" "g" (global i64))
+                           (func (export "f") (result i32)
+                            (i64.eq (get_global 0) (i64.const 37))))`,
+                         {globals: {g: i.exports.g}});
 
     assertEq(j.exports.f(), 1);
 
     // We cannot read or write i64 global values from JS.
 
     let g = i.exports.g;
 
-    assertErrorMessage(() => i.exports.g.value, WebAssembly.LinkError, /cannot pass i64 to or from JS/);
+    assertErrorMessage(() => i.exports.g.value, TypeError, /cannot pass i64 to or from JS/);
 
     // Mutability check comes before i64 check.
     assertErrorMessage(() => i.exports.g.value = 12, TypeError, /can't set value of immutable global/);
-    assertErrorMessage(() => i.exports.h.value = 12, WebAssembly.LinkError, /cannot pass i64 to or from JS/);
+    assertErrorMessage(() => i.exports.h.value = 12, TypeError, /cannot pass i64 to or from JS/);
 }
 
 // Test inner
 var initialValue = '0x123456789abcdef0';
 var nextValue = '0x531642753864975F';
 wasmAssert(`(module
     (global (mut i64) (i64.const ${initialValue}))
     (global i64 (i64.const ${initialValue}))
@@ -334,166 +342,140 @@ wasmAssert(`(module
     dv.setFloat32(0, module.nan32, true);
     assertEq(dv.getUint32(0, true), 0x7fc00000);
 }
 
 // WebAssembly.Global experiment
 
 if (typeof WebAssembly.Global === "function") {
 
+    const Global = WebAssembly.Global;
+
     // These types should work:
-    assertEq(new WebAssembly.Global({type: "i32"}) instanceof WebAssembly.Global, true);
-    assertEq(new WebAssembly.Global({type: "f32"}) instanceof WebAssembly.Global, true);
-    assertEq(new WebAssembly.Global({type: "f64"}) instanceof WebAssembly.Global, true);
+    assertEq(new Global({type: "i32"}) instanceof Global, true);
+    assertEq(new Global({type: "f32"}) instanceof Global, true);
+    assertEq(new Global({type: "f64"}) instanceof Global, true);
 
     // These types should not work:
-    assertErrorMessage(() => new WebAssembly.Global({type: "i64"}),
-                       TypeError,
-                       /bad type for a WebAssembly.Global/);
-    assertErrorMessage(() => new WebAssembly.Global({}),
-                       TypeError,
-                       /bad type for a WebAssembly.Global/);
-    assertErrorMessage(() => new WebAssembly.Global({type: "fnord"}),
-                       TypeError,
-                       /bad type for a WebAssembly.Global/);
-    assertErrorMessage(() => new WebAssembly.Global(),
-                       TypeError,
-                       /WebAssembly.Global requires more than 0 arguments/);
+    assertErrorMessage(() => new Global({type: "i64"}),   TypeError, /bad type for a WebAssembly.Global/);
+    assertErrorMessage(() => new Global({}),              TypeError, /bad type for a WebAssembly.Global/);
+    assertErrorMessage(() => new Global({type: "fnord"}), TypeError, /bad type for a WebAssembly.Global/);
+    assertErrorMessage(() => new Global(),                TypeError, /Global requires more than 0 arguments/);
 
     // Coercion of init value; ".value" accessor
-    assertEq((new WebAssembly.Global({type: "i32", value: 3.14})).value, 3);
-    assertEq((new WebAssembly.Global({type: "f32", value: { valueOf: () => 33.5 }})).value, 33.5);
+    assertEq((new Global({type: "i32", value: 3.14})).value, 3);
+    assertEq((new Global({type: "f32", value: { valueOf: () => 33.5 }})).value, 33.5);
+    assertEq((new Global({type: "f64", value: "3.25"})).value, 3.25);
 
     // Nothing special about NaN, it coerces just fine
-    assertEq((new WebAssembly.Global({type: "i32", value: NaN})).value, 0);
+    assertEq((new Global({type: "i32", value: NaN})).value, 0);
 
     {
         // "value" is enumerable
-        let x = new WebAssembly.Global({type: "i32"});
+        let x = new Global({type: "i32"});
         let s = "";
         for ( let i in x )
             s = s + i + ",";
         assertEq(s, "value,");
     }
 
     // "value" is defined on the prototype, not on the object
-    assertEq("value" in WebAssembly.Global.prototype, true);
+    assertEq("value" in Global.prototype, true);
 
     // Can't set the value of an immutable global
-    assertErrorMessage(() => (new WebAssembly.Global({type: "i32"})).value = 10,
+    assertErrorMessage(() => (new Global({type: "i32"})).value = 10,
                        TypeError,
                        /can't set value of immutable global/);
 
     {
         // Can set the value of a mutable global
-        let g = new WebAssembly.Global({type: "i32", mutable: true, value: 37});
+        let g = new Global({type: "i32", mutable: true, value: 37});
         g.value = 10;
         assertEq(g.value, 10);
     }
 
     {
         // Misc internal conversions
-        let g = new WebAssembly.Global({type: "i32", value: 42});
+        let g = new Global({type: "i32", value: 42});
 
         // valueOf
         assertEq(g - 5, 37);
 
         // @@toStringTag
         assertEq(g.toString(), "[object WebAssembly.Global]");
     }
 
     {
-        // An exported global should appear as a WebAssembly.Global instance:
-        let i =
-            new WebAssembly.Instance(
-                new WebAssembly.Module(
-                    wasmTextToBinary(`(module (global (export "g") i32 (i32.const 42)))`)));
+        // An exported global should appear as a Global instance:
+        let i = wasmEvalText(`(module (global (export "g") i32 (i32.const 42)))`);
 
         assertEq(typeof i.exports.g, "object");
-        assertEq(i.exports.g instanceof WebAssembly.Global, true);
+        assertEq(i.exports.g instanceof Global, true);
 
         // An exported global can be imported into another instance even if
         // it is an object:
-        let j =
-            new WebAssembly.Instance(
-                new WebAssembly.Module(
-                    wasmTextToBinary(`(module
-                        (global (import "" "g") i32)
-                        (func (export "f") (result i32)
-                            (get_global 0)))`)),
-                { "": { "g": i.exports.g }});
+        let j = wasmEvalText(`(module
+                               (global (import "" "g") i32)
+                               (func (export "f") (result i32)
+                                (get_global 0)))`,
+                             { "": { "g": i.exports.g }});
 
         // And when it is then accessed it has the right value:
         assertEq(j.exports.f(), 42);
     }
 
-    // Identity of WebAssembly.Global objects (independent of mutablity).
+    // Identity of Global objects (independent of mutablity).
     {
         // When a global is exported twice, the two objects are the same.
-        let i =
-            new WebAssembly.Instance(
-                new WebAssembly.Module(
-                    wasmTextToBinary(`(module
-                        (global i32 (i32.const 0))
-                        (export "a" global 0)
-                        (export "b" global 0))`)));
+        let i = wasmEvalText(`(module
+                               (global i32 (i32.const 0))
+                               (export "a" global 0)
+                               (export "b" global 0))`);
         assertEq(i.exports.a, i.exports.b);
 
         // When a global is imported and then exported, the exported object is
         // the same as the imported object.
-        let j =
-            new WebAssembly.Instance(
-                new WebAssembly.Module(
-                    wasmTextToBinary(`(module
-                        (import "" "a" (global i32))
-                        (export "x" global 0))`)),
-                { "": {a: i.exports.a}});
+        let j = wasmEvalText(`(module
+                               (import "" "a" (global i32))
+                               (export "x" global 0))`,
+                             { "": {a: i.exports.a}});
 
         assertEq(i.exports.a, j.exports.x);
 
         // When a global is imported twice (ie aliased) and then exported twice,
         // the exported objects are the same, and are also the same as the
         // imported object.
-        let k =
-            new WebAssembly.Instance(
-                new WebAssembly.Module(
-                    wasmTextToBinary(`(module
-                        (import "" "a" (global i32))
-                        (import "" "b" (global i32))
-                        (export "x" global 0)
-                        (export "y" global 1))`)),
-                { "": {a: i.exports.a,
-                    b: i.exports.a}});
+        let k = wasmEvalText(`(module
+                               (import "" "a" (global i32))
+                               (import "" "b" (global i32))
+                               (export "x" global 0)
+                               (export "y" global 1))`,
+                             { "": {a: i.exports.a,
+                                    b: i.exports.a}});
 
         assertEq(i.exports.a, k.exports.x);
         assertEq(k.exports.x, k.exports.y);
     }
 
     // Mutability
     {
-        let i =
-            new WebAssembly.Instance(
-                new WebAssembly.Module(
-                    wasmTextToBinary(`(module
-                        (global (export "g") (mut i32) (i32.const 37))
-                        (func (export "getter") (result i32)
-                            (get_global 0))
-                        (func (export "setter") (param i32)
-                            (set_global 0 (get_local 0))))`)));
+        let i = wasmEvalText(`(module
+                               (global (export "g") (mut i32) (i32.const 37))
+                               (func (export "getter") (result i32)
+                                (get_global 0))
+                               (func (export "setter") (param i32)
+                                (set_global 0 (get_local 0))))`);
 
-        let j =
-            new WebAssembly.Instance(
-                new WebAssembly.Module(
-                    wasmTextToBinary(`(module
-                        (import "" "g" (global (mut i32)))
-                        (func (export "getter") (result i32)
-                            (get_global 0))
-                        (func (export "setter") (param i32)
-                            (set_global 0 (get_local 0))))`)),
-                {"": {g: i.exports.g}});
+        let j = wasmEvalText(`(module
+                               (import "" "g" (global (mut i32)))
+                               (func (export "getter") (result i32)
+                                (get_global 0))
+                               (func (export "setter") (param i32)
+                                (set_global 0 (get_local 0))))`,
+                             {"": {g: i.exports.g}});
 
         // Initial values
         assertEq(i.exports.g.value, 37);
         assertEq(i.exports.getter(), 37);
         assertEq(j.exports.getter(), 37);
 
         // Set in i, observe everywhere
         i.exports.setter(42);
@@ -512,24 +494,53 @@ if (typeof WebAssembly.Global === "funct
         // Set on global object, observe everywhere
         i.exports.g.value = 197;
 
         assertEq(i.exports.g.value, 197);
         assertEq(i.exports.getter(), 197);
         assertEq(j.exports.getter(), 197);
     }
 
-    // Providing a primitive value to an imported global should internally
-    // promote it to a Webassembly.Global object.
+    // Mutability of import declaration and imported value have to match
+    {
+        const mutErr = /imported global mutability mismatch/;
+        const i64Err = /cannot pass i64 to or from JS/;
+
+        let m1 = new Module(wasmTextToBinary(`(module
+                                               (import "m" "g" (global i32)))`));
+
+        // Mutable Global matched to immutable import
+        let gm = new Global({type: "i32", value: 42, mutable: true});
+        assertErrorMessage(() => new Instance(m1, {m: {g: gm}}),
+                           LinkError,
+                           mutErr);
+
+        let m2 = new Module(wasmTextToBinary(`(module
+                                               (import "m" "g" (global (mut i32))))`));
 
-    assertEq(wasmEvalText(`(module
-        (global (import "a" "b") (mut i32))
-        (func (export "get") (result i32) get_global 0)
-    )`, { a: { b: 42 } }).exports.get(), 42);
+        // Immutable Global matched to mutable import
+        let gi = new Global({type: "i32", value: 42, mutable: false});
+        assertErrorMessage(() => new Instance(m2, {m: {g: gi}}),
+                           LinkError,
+                           mutErr);
+
+        // Constant value is the same as immutable Global
+        assertErrorMessage(() => new Instance(m2, {m: {g: 42}}),
+                           LinkError,
+                           mutErr);
+
+        let m3 = new Module(wasmTextToBinary(`(module
+                                               (import "m" "g" (global (mut i64))))`));
+
+        // Check against i64 import before matching mutability
+        assertErrorMessage(() => new Instance(m3, {m: {g: 42}}),
+                           LinkError,
+                           i64Err);
+    }
 
     // TEST THIS LAST
 
     // "value" is deletable
-    assertEq(delete WebAssembly.Global.prototype.value, true);
-    assertEq("value" in WebAssembly.Global.prototype, false);
+    assertEq(delete Global.prototype.value, true);
+    assertEq("value" in Global.prototype, false);
 
     // ADD NO MORE TESTS HERE!
 }
--- a/js/src/jit-test/tests/wasm/regress/bug1450800.js
+++ b/js/src/jit-test/tests/wasm/regress/bug1450800.js
@@ -3,19 +3,21 @@ if (!this.gczeal || !WebAssembly.Global)
 
 gczeal(9, 10);
 function wasmEvalText(str, imports) {
     let binary = wasmTextToBinary(str);
     m = new WebAssembly.Module(binary);
     return new WebAssembly.Instance(m, imports);
 }
 assertEq(wasmEvalText(`(module
-    (global (import "a" "b") (mut i32))
-    (func (export "get") (result i32) get_global 0)
-)`, {  a: { b: 42 }}).exports.get(), 42);
+                        (global (import "a" "b") i32)
+                        (export "g" (global 0))
+                        (func (export "get") (result i32) get_global 0))`,
+                      { a: { b: 42 }}).exports.get(),
+         42);
 for (let v of []) {}
 function testInitExpr(type, initialValue, nextValue, coercion, assertFunc = assertEq) {
     var module = wasmEvalText(`(module
         (import "globals" "a" (global ${type}))
         (global $glob_imm ${type} (get_global 0))
         (export "global_imm" (global $glob_imm))
     )`, {
         globals: {
--- a/js/src/jit/Ion.cpp
+++ b/js/src/jit/Ion.cpp
@@ -2930,17 +2930,20 @@ jit::Invalidate(JSContext* cx, const Rec
 {
     jit::Invalidate(cx->zone()->types, cx->runtime()->defaultFreeOp(), invalid, resetUses,
                     cancelOffThread);
 }
 
 void
 jit::IonScript::invalidate(JSContext* cx, JSScript* script, bool resetUses, const char* reason)
 {
-    MOZ_RELEASE_ASSERT(script->ionScript() == this);
+    // Note: we could short circuit here if we already invalidated this
+    // IonScript, but jit::Invalidate also cancels off-thread compilations of
+    // |script|.
+    MOZ_RELEASE_ASSERT(invalidated() || script->ionScript() == this);
 
     JitSpew(JitSpew_IonInvalidate, " Invalidate IonScript %p: %s", this, reason);
 
     // RecompileInfoVector has inline space for at least one element.
     RecompileInfoVector list;
     MOZ_RELEASE_ASSERT(list.reserve(1));
     list.infallibleEmplaceBack(script, compilationId());
 
--- a/js/src/jit/MacroAssembler.cpp
+++ b/js/src/jit/MacroAssembler.cpp
@@ -2660,16 +2660,22 @@ void
 MacroAssembler::finish()
 {
     if (failureLabel_.used()) {
         bind(&failureLabel_);
         handleFailure();
     }
 
     MacroAssemblerSpecific::finish();
+
+    MOZ_RELEASE_ASSERT(size() <= MaxCodeBytesPerProcess,
+                       "AssemblerBuffer should ensure we don't exceed MaxCodeBytesPerProcess");
+
+    if (bytesNeeded() > MaxCodeBytesPerProcess)
+        setOOM();
 }
 
 void
 MacroAssembler::link(JitCode* code)
 {
     MOZ_ASSERT(!oom());
     linkProfilerCallSites(code);
 }
--- a/js/src/jit/shared/IonAssemblerBuffer.h
+++ b/js/src/jit/shared/IonAssemblerBuffer.h
@@ -190,18 +190,17 @@ class AssemblerBuffer
   public:
     bool isAligned(size_t alignment) const {
         MOZ_ASSERT(mozilla::IsPowerOfTwo(alignment));
         return !(size() & (alignment - 1));
     }
 
   private:
     Slice* newSlice(LifoAlloc& a) {
-        // Clients of IonAssemblerBuffer can only handle a size up to INT_MAX.
-        if (size() + sizeof(Slice) > INT32_MAX) {
+        if (size() > MaxCodeBytesPerProcess - sizeof(Slice)) {
             fail_oom();
             return nullptr;
         }
         Slice* tmp = static_cast<Slice*>(a.alloc(sizeof(Slice)));
         if (!tmp) {
             fail_oom();
             return nullptr;
         }
@@ -293,16 +292,19 @@ class AssemblerBuffer
         return ret;
     }
 
     unsigned int size() const {
         if (tail)
             return bufferSize + tail->length();
         return bufferSize;
     }
+    BufferOffset nextOffset() const {
+        return BufferOffset(size());
+    }
 
     bool oom() const { return m_oom || m_bail; }
     bool bail() const { return m_bail; }
 
     bool fail_oom() {
         m_oom = true;
         return false;
     }
@@ -408,22 +410,16 @@ class AssemblerBuffer
             return getInstForwards(off, head, 0);
 
         // The last slice was already checked above, so start at the
         // second-to-last.
         Slice* prev = tail->getPrev();
         return getInstBackwards(off, prev, bufferSize - prev->length());
     }
 
-    BufferOffset nextOffset() const {
-        if (tail)
-            return BufferOffset(bufferSize + tail->length());
-        return BufferOffset(bufferSize);
-    }
-
     typedef AssemblerBuffer<SliceSize, Inst> ThisClass;
 
     class AssemblerBufferInstIterator
     {
         BufferOffset bo_;
         ThisClass* buffer_;
 
       public:
--- a/js/src/jit/x86-shared/AssemblerBuffer-x86-shared.h
+++ b/js/src/jit/x86-shared/AssemblerBuffer-x86-shared.h
@@ -62,16 +62,43 @@
 #define ADDR_o32r(offset)                      (offset)
 
 namespace js {
 
     class Sprinter;
 
 namespace jit {
 
+    // AllocPolicy for AssemblerBuffer. OOMs when trying to allocate more than
+    // MaxCodeBytesPerProcess bytes. Use private inheritance to make sure we
+    // explicitly have to expose SystemAllocPolicy methods.
+    class AssemblerBufferAllocPolicy : private SystemAllocPolicy
+    {
+      public:
+        using SystemAllocPolicy::checkSimulatedOOM;
+        using SystemAllocPolicy::reportAllocOverflow;
+        using SystemAllocPolicy::free_;
+
+        template <typename T> T* pod_realloc(T* p, size_t oldSize, size_t newSize) {
+            static_assert(sizeof(T) == 1,
+                          "AssemblerBufferAllocPolicy should only be used with byte vectors");
+            MOZ_ASSERT(oldSize <= MaxCodeBytesPerProcess);
+            if (MOZ_UNLIKELY(newSize > MaxCodeBytesPerProcess))
+                return nullptr;
+            return SystemAllocPolicy::pod_realloc<T>(p, oldSize, newSize);
+        }
+        template <typename T> T* pod_malloc(size_t numElems) {
+            static_assert(sizeof(T) == 1,
+                          "AssemblerBufferAllocPolicy should only be used with byte vectors");
+            if (MOZ_UNLIKELY(numElems > MaxCodeBytesPerProcess))
+                return nullptr;
+            return SystemAllocPolicy::pod_malloc<T>(numElems);
+        }
+    };
+
     class AssemblerBuffer
     {
         template<size_t size, typename T>
         MOZ_ALWAYS_INLINE void sizedAppendUnchecked(T value)
         {
             m_buffer.infallibleAppend(reinterpret_cast<unsigned char*>(&value), size);
         }
 
@@ -82,16 +109,19 @@ namespace jit {
                 oomDetected();
         }
 
     public:
         AssemblerBuffer() : m_oom(false) {}
 
         void ensureSpace(size_t space)
         {
+            // This should only be called with small |space| values to ensure
+            // we don't overflow below.
+            MOZ_ASSERT(space <= 16);
             if (MOZ_UNLIKELY(!m_buffer.reserve(m_buffer.length() + space)))
                 oomDetected();
         }
 
         bool isAligned(size_t alignment) const
         {
             return !(m_buffer.length() & (alignment - 1));
         }
@@ -159,17 +189,17 @@ namespace jit {
          * See also the |buffer| method.
          */
         void oomDetected()
         {
             m_oom = true;
             m_buffer.clear();
         }
 
-        mozilla::Vector<unsigned char, 256, SystemAllocPolicy> m_buffer;
+        mozilla::Vector<unsigned char, 256, AssemblerBufferAllocPolicy> m_buffer;
         bool m_oom;
     };
 
     class GenericAssembler
     {
 #ifdef JS_JITSPEW
         Sprinter* printer;
 #endif
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -363,16 +363,17 @@ MSG_DEF(JSMSG_WASM_BAD_IMPORT_TYPE,    2
 MSG_DEF(JSMSG_WASM_BAD_IMPORT_SIG,     2, JSEXN_WASMLINKERROR, "imported function '{0}.{1}' signature mismatch")
 MSG_DEF(JSMSG_WASM_BAD_IMP_SIZE,       1, JSEXN_WASMLINKERROR, "imported {0} with incompatible size")
 MSG_DEF(JSMSG_WASM_BAD_IMP_MAX,        1, JSEXN_WASMLINKERROR, "imported {0} with incompatible maximum size")
 MSG_DEF(JSMSG_WASM_IMP_SHARED_REQD,    0, JSEXN_WASMLINKERROR, "imported unshared memory but shared required")
 MSG_DEF(JSMSG_WASM_IMP_SHARED_BANNED,  0, JSEXN_WASMLINKERROR, "imported shared memory but unshared required")
 MSG_DEF(JSMSG_WASM_BAD_FIT,            2, JSEXN_WASMLINKERROR, "{0} segment does not fit in {1}")
 MSG_DEF(JSMSG_WASM_BAD_I64_LINK,       0, JSEXN_WASMLINKERROR, "cannot pass i64 to or from JS")
 MSG_DEF(JSMSG_WASM_NO_SHMEM_LINK,      0, JSEXN_WASMLINKERROR, "shared memory is disabled")
+MSG_DEF(JSMSG_WASM_BAD_MUT_LINK,       0, JSEXN_WASMLINKERROR, "imported global mutability mismatch")
 MSG_DEF(JSMSG_WASM_IND_CALL_TO_NULL,   0, JSEXN_WASMRUNTIMEERROR, "indirect call to null")
 MSG_DEF(JSMSG_WASM_IND_CALL_BAD_SIG,   0, JSEXN_WASMRUNTIMEERROR, "indirect call signature mismatch")
 MSG_DEF(JSMSG_WASM_UNREACHABLE,        0, JSEXN_WASMRUNTIMEERROR, "unreachable executed")
 MSG_DEF(JSMSG_WASM_INTEGER_OVERFLOW,   0, JSEXN_WASMRUNTIMEERROR, "integer overflow")
 MSG_DEF(JSMSG_WASM_INVALID_CONVERSION, 0, JSEXN_WASMRUNTIMEERROR, "invalid conversion to integer")
 MSG_DEF(JSMSG_WASM_INT_DIVIDE_BY_ZERO, 0, JSEXN_WASMRUNTIMEERROR, "integer divide by zero")
 MSG_DEF(JSMSG_WASM_OUT_OF_BOUNDS,      0, JSEXN_WASMRUNTIMEERROR, "index out of bounds")
 MSG_DEF(JSMSG_WASM_UNALIGNED_ACCESS,   0, JSEXN_WASMRUNTIMEERROR, "unaligned memory access")
--- a/js/src/wasm/WasmJS.cpp
+++ b/js/src/wasm/WasmJS.cpp
@@ -244,35 +244,47 @@ GetImports(JSContext* cx,
             Val val;
             const uint32_t index = globalIndex++;
             const GlobalDesc& global = globals[index];
             MOZ_ASSERT(global.importIndex() == index);
 
 #if defined(ENABLE_WASM_GLOBAL) && defined(EARLY_BETA_OR_EARLIER)
             if (v.isObject() && v.toObject().is<WasmGlobalObject>()) {
                 RootedWasmGlobalObject obj(cx, &v.toObject().as<WasmGlobalObject>());
+
+                if (obj->isMutable() != global.isMutable()) {
+                    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_MUT_LINK);
+                    return false;
+                }
+
                 if (globalObjs.length() <= index && !globalObjs.resize(index + 1)) {
                     ReportOutOfMemory(cx);
                     return false;
                 }
                 globalObjs[index] = obj;
                 val = obj->val();
             } else
 #endif
-            {
+            if (v.isNumber()) {
                 if (global.type() == ValType::I64) {
                     JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_I64_LINK);
                     return false;
                 }
 
-                if (!v.isNumber())
-                    return ThrowBadImportType(cx, import.field.get(), "Number");
+#if defined(ENABLE_WASM_GLOBAL) && defined(EARLY_BETA_OR_EARLIER)
+                if (global.isMutable()) {
+                    JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_MUT_LINK);
+                    return false;
+                }
+#endif
 
                 if (!ToWebAssemblyValue(cx, global.type(), v, &val))
                     return false;
+            } else {
+                return ThrowBadImportType(cx, import.field.get(), "Number");
             }
 
             if (!globalImportValues->append(val))
                 return false;
         }
     }
 
     MOZ_ASSERT(globalIndex == globals.length() || !globals[globalIndex].isImport());
@@ -2173,17 +2185,17 @@ WasmGlobalObject::valueGetterImpl(JSCont
 {
     switch (args.thisv().toObject().as<WasmGlobalObject>().type()) {
       case ValType::I32:
       case ValType::F32:
       case ValType::F64:
         args.rval().set(args.thisv().toObject().as<WasmGlobalObject>().value());
         return true;
       case ValType::I64:
-        JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_I64_LINK);
+        JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_I64_TYPE);
         return false;
       default:
         MOZ_CRASH();
     }
 }
 
 /* static */ bool
 WasmGlobalObject::valueGetter(JSContext* cx, unsigned argc, Value* vp)
@@ -2197,17 +2209,17 @@ WasmGlobalObject::valueSetterImpl(JSCont
 {
     RootedWasmGlobalObject global(cx, &args.thisv().toObject().as<WasmGlobalObject>());
     if (!global->isMutable()) {
         JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_GLOBAL_IMMUTABLE);
         return false;
     }
 
     if (global->type() == ValType::I64) {
-        JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_I64_LINK);
+        JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_I64_TYPE);
         return false;
     }
 
     Val val;
     if (!ToWebAssemblyValue(cx, global->type(), args.get(0), &val))
         return false;
 
     Cell* cell = global->cell();
--- a/js/src/wasm/WasmModule.cpp
+++ b/js/src/wasm/WasmModule.cpp
@@ -1058,34 +1058,34 @@ Module::instantiateGlobals(JSContext* cx
         if (exp.kind() != DefinitionKind::Global)
             continue;
         unsigned globalIndex = exp.globalIndex();
         const GlobalDesc& global = globals[globalIndex];
         if (!EnsureGlobalObject(cx, globalImportValues, globalIndex, global, globalObjs))
             return false;
     }
 
-    // Imported globals may also have received only a primitive value, thus
-    // they may need their own Global object, because the compiled code assumed
-    // they were indirect.
+    // Imported globals that are not re-exported may also have received only a
+    // primitive value; these globals are always immutable.  Assert that we do
+    // not need to create any additional Global objects for such imports.
 
+# ifdef DEBUG
     size_t numGlobalImports = 0;
     for (const Import& import : imports_) {
         if (import.kind != DefinitionKind::Global)
             continue;
         size_t globalIndex = numGlobalImports++;
         const GlobalDesc& global = globals[globalIndex];
         MOZ_ASSERT(global.importIndex() == globalIndex);
-        if (!global.isIndirect())
-            continue;
-        if (!EnsureGlobalObject(cx, globalImportValues, globalIndex, global, globalObjs))
-            return false;
+        MOZ_ASSERT_IF(global.isIndirect(),
+                      globalIndex < globalObjs.length() || globalObjs[globalIndex]);
     }
     MOZ_ASSERT_IF(!metadata().isAsmJS(),
                   numGlobalImports == globals.length() || !globals[numGlobalImports].isImport());
+# endif
 #endif
     return true;
 }
 
 static bool
 GetFunctionExport(JSContext* cx,
                   HandleWasmInstanceObject instanceObj,
                   Handle<FunctionVector> funcImports,
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5749,16 +5749,19 @@ pref("media.block-autoplay-until-in-fore
 // behavior of Firefox.
 pref("security.data_uri.unique_opaque_origin", true);
 
 // If true, all toplevel data: URI navigations will be blocked.
 // Please note that manually entering a data: URI in the
 // URL-Bar will not be blocked when flipping this pref.
 pref("security.data_uri.block_toplevel_data_uri_navigations", true);
 
+// If true, all FTP subresource loads will be blocked.
+pref("security.block_ftp_subresources", true);
+
 // Enable Storage API for all platforms except Android.
 #if !defined(MOZ_WIDGET_ANDROID)
 pref("dom.storageManager.enabled", true);
 #else
 pref("dom.storageManager.enabled", false);
 #endif
 pref("dom.storageManager.prompt.testing", false);
 pref("dom.storageManager.prompt.testing.allow", false);
--- a/netwerk/base/nsIOService.cpp
+++ b/netwerk/base/nsIOService.cpp
@@ -169,16 +169,17 @@ static const char kProfileChangeNetResto
 static const char kProfileDoChange[] = "profile-do-change";
 
 // Necko buffer defaults
 uint32_t   nsIOService::gDefaultSegmentSize = 4096;
 uint32_t   nsIOService::gDefaultSegmentCount = 24;
 
 bool nsIOService::sIsDataURIUniqueOpaqueOrigin = false;
 bool nsIOService::sBlockToplevelDataUriNavigations = false;
+bool nsIOService::sBlockFTPSubresources = false;
 
 ////////////////////////////////////////////////////////////////////////////////
 
 nsIOService::nsIOService()
     : mOffline(true)
     , mOfflineForProfileChange(false)
     , mManageLinkStatus(false)
     , mConnectivity(true)
@@ -242,16 +243,18 @@ nsIOService::Init()
     }
     else
         NS_WARNING("failed to get observer service");
 
     Preferences::AddBoolVarCache(&sIsDataURIUniqueOpaqueOrigin,
                                  "security.data_uri.unique_opaque_origin", false);
     Preferences::AddBoolVarCache(&sBlockToplevelDataUriNavigations,
                                  "security.data_uri.block_toplevel_data_uri_navigations", false);
+    Preferences::AddBoolVarCache(&sBlockFTPSubresources,
+                                 "security.block_ftp_subresources", true);
     Preferences::AddBoolVarCache(&mOfflineMirrorsConnectivity, OFFLINE_MIRRORS_CONNECTIVITY, true);
 
     gIOService = this;
 
     InitializeNetworkLinkService();
     InitializeProtocolProxyService();
 
     SetOffline(false);
@@ -1923,16 +1926,22 @@ nsIOService::IsDataURIUniqueOpaqueOrigin
 }
 
 /*static*/ bool
 nsIOService::BlockToplevelDataUriNavigations()
 {
   return sBlockToplevelDataUriNavigations;
 }
 
+/*static*/ bool
+nsIOService::BlockFTPSubresources()
+{
+  return sBlockFTPSubresources;
+}
+
 NS_IMETHODIMP
 nsIOService::NotImplemented()
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 } // namespace net
 } // namespace mozilla
--- a/netwerk/base/nsIOService.h
+++ b/netwerk/base/nsIOService.h
@@ -90,16 +90,18 @@ public:
     // reset mHttpHandlerAlreadyShutingDown.
     void SetHttpHandlerAlreadyShutingDown();
 
     bool IsLinkUp();
 
     static bool IsDataURIUniqueOpaqueOrigin();
     static bool BlockToplevelDataUriNavigations();
 
+    static bool BlockFTPSubresources();
+
     // Used to count the total number of HTTP requests made
     void IncrementRequestNumber() { mTotalRequests++; }
     uint32_t GetTotalRequestNumber() { return mTotalRequests; }
     // Used to keep "race cache with network" stats
     void IncrementCacheWonRequestNumber() { mCacheWon++; }
     uint32_t GetCacheWonRequestNumber() { return mCacheWon; }
     void IncrementNetWonRequestNumber() { mNetWon++; }
     uint32_t GetNetWonRequestNumber() { return mNetWon; }
@@ -192,16 +194,18 @@ private:
 
     nsTArray<int32_t>                    mRestrictedPortList;
 
     bool                                 mNetworkNotifyChanged;
 
     static bool                          sIsDataURIUniqueOpaqueOrigin;
     static bool                          sBlockToplevelDataUriNavigations;
 
+    static bool                          sBlockFTPSubresources;
+
     uint32_t mTotalRequests;
     uint32_t mCacheWon;
     uint32_t mNetWon;
 
     // These timestamps are needed for collecting telemetry on PR_Connect,
     // PR_ConnectContinue and PR_Close blocking time.  If we spend very long
     // time in any of these functions we want to know if and what network
     // change has happened shortly before.
--- a/taskcluster/docker/periodic-updates/scripts/periodic_file_updates.sh
+++ b/taskcluster/docker/periodic-updates/scripts/periodic_file_updates.sh
@@ -407,17 +407,17 @@ function push_repo {
   if [ -z "${REVIEWERS}" ]
   then
     return 1
   fi
 
   # Clean up older review requests
   # Turn  Needs Review D624: No bug, Automated HSTS ...
   # into D624
-  for diff in $($ARC list | grep "Needs Review" | grep -E "Automated HSTS|Automated HPKP|Automated blocklist" | awk 'match($0, /D[0-9]+[^: ]/, arr) { print arr[0] }')
+  for diff in $($ARC list | grep "Needs Review" | grep -E "Automated HSTS|Automated HPKP|Automated blocklist" | awk 'match($0, /D[0-9]+[^: ]/) { print substr($0, RSTART, RLENGTH)  }')
   do
     echo "Removing old request $diff"
     # There is no 'arc abandon', see bug 1452082
     echo '{"transactions": [{"type":"abandon"}], "objectIdentifier": "'"${diff}"'"}' | arc call-conduit differential.revision.edit
   done
 
   $ARC diff --verbatim --reviewers "${REVIEWERS}"
 }
--- a/taskcluster/taskgraph/util/attributes.py
+++ b/taskcluster/taskgraph/util/attributes.py
@@ -7,22 +7,24 @@ from __future__ import absolute_import, 
 import re
 
 
 INTEGRATION_PROJECTS = {
     'mozilla-inbound',
     'autoland',
 }
 
-TRUNK_PROJECTS = INTEGRATION_PROJECTS | {'mozilla-central', }
+TRUNK_PROJECTS = INTEGRATION_PROJECTS | {'mozilla-central', 'comm-central'}
 
 RELEASE_PROJECTS = {
     'mozilla-central',
     'mozilla-beta',
     'mozilla-release',
+    'comm-central',
+    'comm-beta',
 }
 
 RELEASE_PROMOTION_PROJECTS = {
     'jamun',
     'maple',
     'try',
 } | RELEASE_PROJECTS
 
--- a/toolkit/components/passwordmgr/content/passwordManager.js
+++ b/toolkit/components/passwordmgr/content/passwordManager.js
@@ -663,17 +663,17 @@ function EditCellInSelectedRow(columnNam
   let row = signonsTree.currentIndex;
   let columnElement = getColumnByName(columnName);
   signonsTree.startEditing(row, signonsTree.columns.getColumnFor(columnElement));
 }
 
 function LaunchSiteUrl() {
   let row = signonsTree.currentIndex;
   let url = signonsTreeView.getCellText(row, {id: "siteCol"});
-  window.openUILinkIn(url, "tab");
+  window.openWebLinkIn(url, "tab");
 }
 
 function UpdateContextMenu() {
   let singleSelection = (signonsTreeView.selection.count == 1);
   let menuItems = new Map();
   let menupopup = document.getElementById("signonsTreeContextMenu");
   for (let menuItem of menupopup.querySelectorAll("menuitem")) {
     menuItems.set(menuItem.id, menuItem);
@@ -684,17 +684,17 @@ function UpdateContextMenu() {
       menuItem.setAttribute("disabled", "true");
     }
     return;
   }
 
   let selectedRow = signonsTree.currentIndex;
 
   // Don't display "Launch Site URL" if we're not a browser.
-  if (window.openUILinkIn) {
+  if (window.openWebLinkIn) {
     menuItems.get("context-launchsiteurl").removeAttribute("disabled");
   } else {
     menuItems.get("context-launchsiteurl").setAttribute("disabled", "true");
     menuItems.get("context-launchsiteurl").setAttribute("hidden", "true");
   }
 
   // Disable "Copy Username" if the username is empty.
   if (signonsTreeView.getCellText(selectedRow, { id: "userCol" }) != "") {
--- a/toolkit/components/viewsource/content/viewSourceUtils.js
+++ b/toolkit/components/viewsource/content/viewSourceUtils.js
@@ -69,17 +69,17 @@ var gViewSourceUtils = {
       Services.ww.unregisterNotification(onOpen);
       let win = subj.QueryInterface(Ci.nsIInterfaceRequestor)
                     .getInterface(Ci.nsIDOMWindow);
       win.addEventListener("load", () => {
         aArgs.viewSourceBrowser = win.gBrowser.selectedTab.linkedBrowser;
         utils.viewSourceInBrowser(aArgs);
       }, { once: true });
     });
-    window.top.openUILinkIn("about:blank", "current");
+    window.top.openWebLinkIn("about:blank", "current");
   },
 
   /**
    * Displays view source in the provided <browser>.  This allows for non-window
    * display methods, such as a tab from Firefox.
    *
    * @param aArgs
    *        An object with the following properties:
--- a/toolkit/content/contentAreaUtils.js
+++ b/toolkit/content/contentAreaUtils.js
@@ -1175,17 +1175,19 @@ function openURL(aURL) {
                       .getService(Ci.nsIExternalProtocolService);
 
   if (!protocolSvc.isExposedProtocol(uri.scheme)) {
     // If we're not a browser, use the external protocol service to load the URI.
     protocolSvc.loadURI(uri);
   } else {
     var recentWindow = Services.wm.getMostRecentWindow("navigator:browser");
     if (recentWindow) {
-      recentWindow.openUILinkIn(uri.spec, "tab");
+      recentWindow.openWebLinkIn(uri.spec, "tab", {
+        triggeringPrincipal: recentWindow.document.contentPrincipal
+      });
       return;
     }
 
     var loadgroup = Cc["@mozilla.org/network/load-group;1"]
                       .createInstance(Ci.nsILoadGroup);
     var appstartup = Services.startup;
 
     var loadListener = {
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1243,17 +1243,17 @@
                            xbl:inherits="selected"/>
         </xul:description>
       </xul:hbox>
     </content>
 
     <handlers>
       <handler event="click" button="0"><![CDATA[
         let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
-        window.openUILinkIn(baseURL + "insecure-password", "tab", {
+        window.openTrustedLinkIn(baseURL + "insecure-password", "tab", {
           relatedToCurrent: true,
         });
       ]]></handler>
     </handlers>
 
     <implementation>
       <constructor><![CDATA[
         // Unlike other autocomplete items, the height of the insecure warning