Bug 1576918 - Port PageStyle actor, which handles alternative stylesheets, to JSWindowActors for Fission compatibility. r=mconley,Gijs
☠☠ backed out by e3ce16ac9143 ☠ ☠
authorJames Jahns <jahnsjam@msu.edu>
Thu, 24 Oct 2019 19:53:42 +0000
changeset 498906 530bcc00246e68a35fe03c19269d4282b9a0e8af
parent 498905 b32c9765011588adbb965332ce0f7e8b3b7f4b30
child 498907 d4b62d6f1e64896701f193f7414ded2033ad2814
push id36732
push userdluca@mozilla.com
push dateFri, 25 Oct 2019 09:55:46 +0000
treeherdermozilla-central@86d60b2e9638 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley, Gijs
bugs1576918
milestone72.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
Bug 1576918 - Port PageStyle actor, which handles alternative stylesheets, to JSWindowActors for Fission compatibility. r=mconley,Gijs Differential Revision: https://phabricator.services.mozilla.com/D46861
browser/actors/PageStyleChild.jsm
browser/actors/PageStyleParent.jsm
browser/actors/moz.build
browser/base/content/browser.js
browser/base/content/test/general/browser_page_style_menu.js
browser/base/content/test/general/browser_page_style_menu_update.js
browser/base/content/test/general/head.js
browser/components/BrowserGlue.jsm
--- a/browser/actors/PageStyleChild.jsm
+++ b/browser/actors/PageStyleChild.jsm
@@ -1,108 +1,95 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
 
 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 var EXPORTED_SYMBOLS = ["PageStyleChild"];
 
-const { ActorChild } = ChromeUtils.import(
-  "resource://gre/modules/ActorChild.jsm"
-);
-
-class PageStyleChild extends ActorChild {
-  getViewer(content) {
-    return content.docShell.contentViewer;
-  }
+class PageStyleChild extends JSWindowActorChild {
+  handleEvent(event) {
+    // On page show, tell the parent all of the stylesheets this document has.
+    if (event.type == "pageshow") {
+      // If we are in the topmost browsing context,
+      // delete the stylesheets from the previous page.
+      if (this.browsingContext.top === this.browsingContext) {
+        this.sendAsyncMessage("PageStyle:Clear");
+      }
 
-  sendStyleSheetInfo(mm) {
-    let content = mm.content;
-    content.requestIdleCallback(() => {
-      let filteredStyleSheets = this._filterStyleSheets(
-        this.getAllStyleSheets(content),
-        content
-      );
+      let window = event.target.ownerGlobal;
+      window.requestIdleCallback(() => {
+        if (!window || window.closed) {
+          return;
+        }
+        let styleSheets = Array.from(this.document.styleSheets);
+        let filteredStyleSheets = this._filterStyleSheets(styleSheets, window);
 
-      mm.sendAsyncMessage("PageStyle:StyleSheets", {
-        filteredStyleSheets,
-        authorStyleDisabled: this.getViewer(content).authorStyleDisabled,
-        preferredStyleSheetSet: content.document.preferredStyleSheetSet,
+        this.sendAsyncMessage("PageStyle:Add", {
+          filteredStyleSheets,
+          authorStyleDisabled: this.docShell.contentViewer.authorStyleDisabled,
+          preferredStyleSheetSet: this.document.preferredStyleSheetSet,
+        });
       });
-    });
-  }
-
-  getAllStyleSheets(frameset) {
-    let selfSheets = Array.from(frameset.document.styleSheets);
-    let subSheets = Array.from(frameset.frames, frame =>
-      this.getAllStyleSheets(frame)
-    );
-    return selfSheets.concat(...subSheets);
+    }
   }
 
   receiveMessage(msg) {
-    let content = msg.target.content;
     switch (msg.name) {
+      // Sent when the page's enabled style sheet is changed.
       case "PageStyle:Switch":
-        this.getViewer(content).authorStyleDisabled = false;
-        this._stylesheetSwitchAll(content, msg.data.title);
-        break;
-
-      case "PageStyle:Disable":
-        this.getViewer(content).authorStyleDisabled = true;
+        this.docShell.contentViewer.authorStyleDisabled = false;
+        this._switchStylesheet(msg.data.title);
         break;
-    }
-
-    this.sendStyleSheetInfo(msg.target);
-  }
-
-  handleEvent(event) {
-    let win = event.target.ownerGlobal;
-    if (win != win.top) {
-      return;
-    }
-
-    let mm = win.docShell.messageManager;
-    this.sendStyleSheetInfo(mm);
-  }
-
-  _stylesheetSwitchAll(frameset, title) {
-    if (!title || this._stylesheetInFrame(frameset, title)) {
-      this._stylesheetSwitchFrame(frameset, title);
-    }
-
-    for (let i = 0; i < frameset.frames.length; i++) {
-      // Recurse into sub-frames.
-      this._stylesheetSwitchAll(frameset.frames[i], title);
+      // Sent when "No Style" is chosen.
+      case "PageStyle:Disable":
+        this.docShell.contentViewer.authorStyleDisabled = true;
+        break;
     }
   }
 
-  _stylesheetSwitchFrame(frame, title) {
-    var docStyleSheets = frame.document.styleSheets;
+  /**
+   * Switch the stylesheet so that only the sheet with the given title is enabled.
+   */
+  _switchStylesheet(title) {
+    let docStyleSheets = this.document.styleSheets;
 
-    for (let i = 0; i < docStyleSheets.length; ++i) {
-      let docStyleSheet = docStyleSheets[i];
+    // Does this doc contain a stylesheet with this title?
+    // If not, it's a subframe's stylesheet that's being changed,
+    // so no need to disable stylesheets here.
+    let docContainsStyleSheet = false;
+    for (let docStyleSheet of docStyleSheets) {
+      if (docStyleSheet.title === title) {
+        docContainsStyleSheet = true;
+        break;
+      }
+    }
+
+    for (let docStyleSheet of docStyleSheets) {
       if (docStyleSheet.title) {
-        docStyleSheet.disabled = docStyleSheet.title != title;
+        if (docContainsStyleSheet) {
+          docStyleSheet.disabled = docStyleSheet.title !== title;
+        }
       } else if (docStyleSheet.disabled) {
         docStyleSheet.disabled = false;
       }
     }
   }
 
-  _stylesheetInFrame(frame, title) {
-    return Array.from(frame.document.styleSheets).some(
-      styleSheet => styleSheet.title == title
-    );
-  }
-
+  /**
+   * Filter the stylesheets that actually apply to this webpage.
+   * @param styleSheets The list of stylesheets from the document.
+   * @param content     The window object that the webpage lives in.
+   */
   _filterStyleSheets(styleSheets, content) {
     let result = [];
 
+    // Only stylesheets with a title can act as an alternative stylesheet.
     for (let currentStyleSheet of styleSheets) {
       if (!currentStyleSheet.title) {
         continue;
       }
 
       // Skip any stylesheets that don't match the screen media type.
       if (currentStyleSheet.media.length) {
         let mediaQueryList = currentStyleSheet.media.mediaText;
@@ -110,17 +97,17 @@ class PageStyleChild extends ActorChild 
           continue;
         }
       }
 
       let URI;
       try {
         if (
           !currentStyleSheet.ownerNode ||
-          // special-case style nodes, which have no href
+          // Special-case style nodes, which have no href.
           currentStyleSheet.ownerNode.nodeName.toLowerCase() != "style"
         ) {
           URI = Services.io.newURI(currentStyleSheet.href);
         }
       } catch (e) {
         if (e.result != Cr.NS_ERROR_MALFORMED_URI) {
           throw e;
         }
new file mode 100644
--- /dev/null
+++ b/browser/actors/PageStyleParent.jsm
@@ -0,0 +1,33 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["PageStyleParent"];
+
+class PageStyleParent extends JSWindowActorParent {
+  receiveMessage(msg) {
+    // The top browser.
+    let browser = this.browsingContext.top.embedderElement;
+    let permanentKey = browser.permanentKey;
+    let window = browser.ownerGlobal;
+    if (window.closed) {
+      return;
+    }
+    let styleMenu = window.gPageStyleMenu;
+
+    switch (msg.name) {
+      case "PageStyle:Add":
+        if (browser.outerBrowser) {
+          // We are in RDM mode and we probably
+          // want to work with the outer browser.
+          browser = browser.outerBrowser;
+        }
+        styleMenu.addBrowserStyleSheets(msg.data, permanentKey);
+        break;
+      case "PageStyle:Clear":
+        styleMenu.clearBrowserStyleSheets(permanentKey);
+        break;
+    }
+  }
+}
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -36,16 +36,17 @@ FINAL_TARGET_FILES.actors += [
     'FormValidationChild.jsm',
     'FormValidationParent.jsm',
     'LightweightThemeChild.jsm',
     'LinkHandlerChild.jsm',
     'NetErrorChild.jsm',
     'OfflineAppsChild.jsm',
     'PageInfoChild.jsm',
     'PageStyleChild.jsm',
+    'PageStyleParent.jsm',
     'PluginChild.jsm',
     'PluginParent.jsm',
     'PromptParent.jsm',
     'RFPHelperChild.jsm',
     'RFPHelperParent.jsm',
     'SearchTelemetryChild.jsm',
     'SwitchDocumentDirectionChild.jsm',
     'URIFixupChild.jsm',
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1803,17 +1803,16 @@ var gBrowserInit = {
 
     window.addEventListener("AppCommand", HandleAppCommandEvent, true);
 
     // These routines add message listeners. They must run before
     // loading the frame script to ensure that we don't miss any
     // message sent between when the frame script is loaded and when
     // the listener is registered.
     DOMEventHandler.init();
-    gPageStyleMenu.init();
     LanguageDetectionListener.init();
     BrowserOnClick.init();
     CaptivePortalWatcher.init();
     ZoomUI.init(window);
 
     let mm = window.getGroupMessageManager("browsers");
     mm.loadFrameScript("chrome://browser/content/tab-content.js", true, true);
     mm.loadFrameScript("chrome://browser/content/content.js", true, true);
@@ -7725,27 +7724,38 @@ var gPageStyleMenu = {
   //   the current page.
   //
   // preferredStyleSheetSet (bool):
   //   Whether or not the user currently has the "Default" style selected
   //   for the current page.
   //
   _pageStyleSheets: new WeakMap(),
 
-  init() {
-    let mm = window.messageManager;
-    mm.addMessageListener("PageStyle:StyleSheets", msg => {
-      if (msg.target.permanentKey) {
-        this._pageStyleSheets.set(msg.target.permanentKey, msg.data);
-      }
-    });
+  /**
+   * Add/append styleSheets to the _pageStyleSheets weakmap.
+   * @param styleSheets
+   *        The stylesheets to add, including the preferred
+   *        stylesheet set for this document.
+   * @param permanentKey
+   *        The permanent key of the browser that
+   *        these stylesheets come from.
+   */
+  addBrowserStyleSheets(styleSheets, permanentKey) {
+    let sheetData = this._pageStyleSheets.get(permanentKey);
+    if (!sheetData) {
+      this._pageStyleSheets.set(permanentKey, styleSheets);
+      return;
+    }
+    sheetData.filteredStyleSheets.push(...styleSheets.filteredStyleSheets);
+    sheetData.preferredStyleSheetSet =
+      sheetData.preferredStyleSheetSet || styleSheets.preferredStyleSheetSet;
   },
 
   /**
-   * Returns an array of Objects representing stylesheets in a
+   * Return an array of Objects representing stylesheets in a
    * browser. Note that the pageshow event needs to fire in content
    * before this information will be available.
    *
    * @param browser (optional)
    *        The <xul:browser> to search for stylesheets. If omitted, this
    *        defaults to the currently selected tab's browser.
    * @returns Array
    *        An Array of Objects representing stylesheets in the browser.
@@ -7759,16 +7769,20 @@ var gPageStyleMenu = {
 
     let data = this._pageStyleSheets.get(browser.permanentKey);
     if (!data) {
       return [];
     }
     return data.filteredStyleSheets;
   },
 
+  clearBrowserStyleSheets(permanentKey) {
+    this._pageStyleSheets.delete(permanentKey);
+  },
+
   _getStyleSheetInfo(browser) {
     let data = this._pageStyleSheets.get(browser.permanentKey);
     if (!data) {
       return {
         filteredStyleSheets: [],
         authorStyleDisabled: false,
         preferredStyleSheetSet: true,
       };
@@ -7827,24 +7841,66 @@ var gPageStyleMenu = {
     noStyle.setAttribute("checked", styleDisabled);
     persistentOnly.setAttribute("checked", !altStyleSelected && !styleDisabled);
     persistentOnly.hidden = styleSheetInfo.preferredStyleSheetSet
       ? haveAltSheets
       : false;
     sep.hidden = (noStyle.hidden && persistentOnly.hidden) || !haveAltSheets;
   },
 
+  /**
+   * Send a message to all PageStyleParents by walking the BrowsingContext tree.
+   * @param message
+   *        The string message to send to each PageStyleChild.
+   * @param data
+   *        The data to send to each PageStyleChild within the message.
+   */
+  _sendMessageToAll(message, data) {
+    let contextsToVisit = [gBrowser.selectedBrowser.browsingContext];
+    while (contextsToVisit.length) {
+      let currentContext = contextsToVisit.pop();
+      let global = currentContext.currentWindowGlobal;
+
+      if (!global) {
+        continue;
+      }
+
+      let actor = global.getActor("PageStyle");
+      actor.sendAsyncMessage(message, data);
+
+      contextsToVisit.push(...currentContext.getChildren());
+    }
+  },
+
+  /**
+   * Switch the stylesheet of all documents in the current browser.
+   * @param title The title of the stylesheet to switch to.
+   */
   switchStyleSheet(title) {
-    let mm = gBrowser.selectedBrowser.messageManager;
-    mm.sendAsyncMessage("PageStyle:Switch", { title });
-  },
-
+    let { permanentKey } = gBrowser.selectedBrowser;
+    let sheetData = this._pageStyleSheets.get(permanentKey);
+    if (sheetData && sheetData.filteredStyleSheets) {
+      sheetData.authorStyleDisabled = false;
+      for (let sheet of sheetData.filteredStyleSheets) {
+        sheet.disabled = sheet.title !== title;
+      }
+    }
+    this._sendMessageToAll("PageStyle:Switch", { title });
+  },
+
+  /**
+   * Disable all stylesheets. Called with View > Page Style > No Style.
+   */
   disableStyle() {
-    let mm = gBrowser.selectedBrowser.messageManager;
-    mm.sendAsyncMessage("PageStyle:Disable");
+    let { permanentKey } = gBrowser.selectedBrowser;
+    let sheetData = this._pageStyleSheets.get(permanentKey);
+    if (sheetData) {
+      sheetData.authorStyleDisabled = true;
+    }
+    this._sendMessageToAll("PageStyle:Disable", {});
   },
 };
 
 var LanguageDetectionListener = {
   init() {
     window.messageManager.addMessageListener(
       "Translation:DocumentState",
       msg => {
--- a/browser/base/content/test/general/browser_page_style_menu.js
+++ b/browser/base/content/test/general/browser_page_style_menu.js
@@ -10,17 +10,17 @@ const PAGE =
 add_task(async function() {
   let tab = await BrowserTestUtils.openNewForegroundTab(
     gBrowser,
     "about:blank",
     false
   );
   let browser = tab.linkedBrowser;
   await BrowserTestUtils.loadURI(browser, PAGE);
-  await promiseStylesheetsUpdated(browser);
+  await promiseStylesheetsLoaded(tab, 17);
 
   let menupopup = document.getElementById("pageStyleMenu").menupopup;
   gPageStyleMenu.fillPopup(menupopup);
 
   var items = [];
   var current = menupopup.getElementsByTagName("menuseparator")[0];
   while (current.nextElementSibling) {
     current = current.nextElementSibling;
--- a/browser/base/content/test/general/browser_page_style_menu_update.js
+++ b/browser/base/content/test/general/browser_page_style_menu_update.js
@@ -9,19 +9,18 @@ const PAGE =
  */
 add_task(async function() {
   let tab = await BrowserTestUtils.openNewForegroundTab(
     gBrowser,
     "about:blank",
     false
   );
   let browser = tab.linkedBrowser;
-
   await BrowserTestUtils.loadURI(browser, PAGE);
-  await promiseStylesheetsUpdated(browser);
+  await promiseStylesheetsLoaded(tab, 17);
 
   let menupopup = document.getElementById("pageStyleMenu").menupopup;
   gPageStyleMenu.fillPopup(menupopup);
 
   // page_style_sample.html should default us to selecting the stylesheet
   // with the title "6" first.
   let selected = menupopup.querySelector("menuitem[checked='true']");
   is(
@@ -29,20 +28,16 @@ add_task(async function() {
     "6",
     "Should have '6' stylesheet selected by default"
   );
 
   // Now select stylesheet "1"
   let target = menupopup.querySelector("menuitem[label='1']");
   target.click();
 
-  // Now we need to wait for the content process to send its stylesheet
-  // update for the selected tab to the parent.
-  await promiseStylesheetsUpdated(browser);
-
   gPageStyleMenu.fillPopup(menupopup);
   // gPageStyleMenu empties out the menu between opens, so we need
   // to get a new reference to the selected menuitem
   selected = menupopup.querySelector("menuitem[checked='true']");
   is(
     selected.getAttribute("label"),
     "1",
     "Should now have stylesheet 1 selected"
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -525,23 +525,25 @@ async function loadBadCertPage(url) {
 
   await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
     content.document.getElementById("exceptionDialogButton").click();
   });
   await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
 }
 
 /**
- * Waits for the message from content to update the Page Style menu.
+ * Waits for the stylesheets to be loaded into the browser menu.
  *
- * @param browser
- *        The <xul:browser> to wait for.
+ * @param tab
+ *        The tab that contains the webpage we're testing.
+ * @param styleSheetCount
+ *        How many stylesheets we expect to be loaded.
  * @return Promise
  */
-async function promiseStylesheetsUpdated(browser) {
-  await BrowserTestUtils.waitForMessage(
-    browser.messageManager,
-    "PageStyle:StyleSheets"
-  );
-  // Resolve on the next tick of the event loop to give the Page Style
-  // menu code an opportunity to update.
-  await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+async function promiseStylesheetsLoaded(tab, styleSheetCount) {
+  let styleMenu = tab.ownerGlobal.gPageStyleMenu;
+  let permanentKey = tab.permanentKey;
+
+  await TestUtils.waitForCondition(() => {
+    let menu = styleMenu._pageStyleSheets.get(permanentKey);
+    return menu && menu.filteredStyleSheets.length >= styleSheetCount;
+  }, "waiting for style sheets to load");
 }
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -99,16 +99,33 @@ let ACTORS = {
   PageInfo: {
     child: {
       moduleURI: "resource:///actors/PageInfoChild.jsm",
     },
 
     allFrames: true,
   },
 
+  PageStyle: {
+    parent: {
+      moduleURI: "resource:///actors/PageStyleParent.jsm",
+    },
+    child: {
+      moduleURI: "resource:///actors/PageStyleChild.jsm",
+      events: {
+        pageshow: {},
+      },
+    },
+
+    // Only matching web pages, as opposed to internal about:, chrome: or
+    // resource: pages. See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
+    matches: ["*://*/*"],
+    allFrames: true,
+  },
+
   Plugin: {
     parent: {
       moduleURI: "resource:///actors/PluginParent.jsm",
     },
     child: {
       moduleURI: "resource:///actors/PluginChild.jsm",
       events: {
         PluginBindingAttached: { capture: true, wantUntrusted: true },
@@ -308,30 +325,16 @@ let LEGACY_ACTORS = {
       module: "resource:///actors/OfflineAppsChild.jsm",
       events: {
         MozApplicationManifest: {},
       },
       messages: ["OfflineApps:StartFetching"],
     },
   },
 
-  PageStyle: {
-    child: {
-      module: "resource:///actors/PageStyleChild.jsm",
-      group: "browsers",
-      events: {
-        pageshow: {},
-      },
-      messages: ["PageStyle:Switch", "PageStyle:Disable"],
-      // Only matching web pages, as opposed to internal about:, chrome: or
-      // resource: pages. See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
-      matches: ["*://*/*"],
-    },
-  },
-
   SearchTelemetry: {
     child: {
       module: "resource:///actors/SearchTelemetryChild.jsm",
       events: {
         DOMContentLoaded: {},
         pageshow: { mozSystemGroup: true },
       },
     },