Bug 621158 - make appcache use messaging for quota management, r?mayhemer,jaws draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Mon, 29 Feb 2016 17:52:35 +0000
changeset 339930 343aab5cb460641d4fb0dd21760ec40cc68e9c28
parent 339874 f0c0480732d36153e8839c7f17394d45f679f87d
child 516081 caf3dc84a39a23248f15cfced2b0ee883e91d411
push id12833
push usergijskruitbosch@gmail.com
push dateMon, 14 Mar 2016 12:11:01 +0000
reviewersmayhemer, jaws
bugs621158
milestone48.0a1
Bug 621158 - make appcache use messaging for quota management, r?mayhemer,jaws MozReview-Commit-ID: GfHbERuzuW8
browser/base/content/browser.js
browser/base/content/content.js
browser/base/content/test/general/browser_offlineQuotaNotification.js
toolkit/content/browser-content.js
toolkit/content/widgets/browser.xml
toolkit/content/widgets/remote-browser.xml
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1044,26 +1044,19 @@ var gBrowserInit = {
   _delayedStartup: function() {
     let tmp = {};
     Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp);
     let TelemetryTimestamps = tmp.TelemetryTimestamps;
     TelemetryTimestamps.add("delayedStartupStarted");
 
     this._cancelDelayedStartup();
 
-    // We need to set the MozApplicationManifest event listeners up
-    // before we start loading the home pages in case a document has
-    // a "manifest" attribute, in which the MozApplicationManifest event
-    // will be fired.
-    gBrowser.addEventListener("MozApplicationManifest",
-                              OfflineApps, false);
-    // listen for offline apps on social
-    let socialBrowser = document.getElementById("social-sidebar-browser");
-    socialBrowser.addEventListener("MozApplicationManifest",
-                              OfflineApps, false);
+    // We need to set the OfflineApps message listeners up before we
+    // load homepages, which might need them.
+    OfflineApps.init();
 
     // This pageshow listener needs to be registered before we may call
     // swapBrowsersAndCloseOther() to receive pageshow events fired by that.
     let mm = window.messageManager;
     mm.addMessageListener("PageVisibility:Show", function(message) {
       if (message.target == gBrowser.selectedBrowser) {
         setTimeout(pageShowEventHandlers, 0, message.data.persisted);
       }
@@ -1171,17 +1164,16 @@ var gBrowserInit = {
     Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-origin-blocked", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false);
     window.messageManager.addMessageListener("Browser:URIFixup", gKeywordURIFixup);
 
     BrowserOffline.init();
-    OfflineApps.init();
     IndexedDBPromptHelper.init();
 
     if (AppConstants.E10S_TESTING_ONLY)
       gRemoteTabsUI.init();
 
     // Initialize the full zoom setting.
     // We do this before the session restore service gets initialized so we can
     // apply full zoom settings to tabs restored by the session restore service.
@@ -1493,17 +1485,16 @@ var gBrowserInit = {
         Cu.reportError(ex);
       }
 
       if (this.gmpInstallManager) {
         this.gmpInstallManager.uninit();
       }
 
       BrowserOffline.uninit();
-      OfflineApps.uninit();
       IndexedDBPromptHelper.uninit();
       LightweightThemeListener.uninit();
       PanelUI.uninit();
     }
 
     // Final window teardown, do this last.
     window.XULBrowserWindow = null;
     window.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -5740,283 +5731,153 @@ var BrowserOffline = {
     if (offlineLocked)
       this._uiElement.setAttribute("disabled", "true");
 
     this._uiElement.setAttribute("checked", aOffline);
   }
 };
 
 var OfflineApps = {
-  /////////////////////////////////////////////////////////////////////////////
-  // OfflineApps Public Methods
-  init: function ()
-  {
-    Services.obs.addObserver(this, "offline-cache-update-completed", false);
-  },
-
-  uninit: function ()
-  {
-    Services.obs.removeObserver(this, "offline-cache-update-completed");
-  },
-
-  handleEvent: function(event) {
-    if (event.type == "MozApplicationManifest") {
-      this.offlineAppRequested(event.originalTarget.defaultView);
-    }
-  },
-
-  /////////////////////////////////////////////////////////////////////////////
-  // OfflineApps Implementation Methods
-
-  // XXX: _getBrowserWindowForContentWindow and _getBrowserForContentWindow
-  // were taken from browser/components/feeds/WebContentConverter.
-  _getBrowserWindowForContentWindow: function(aContentWindow) {
-    return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIWebNavigation)
-                         .QueryInterface(Ci.nsIDocShellTreeItem)
-                         .rootTreeItem
-                         .QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindow)
-                         .wrappedJSObject;
-  },
-
-  _getBrowserForContentWindow: function(aBrowserWindow, aContentWindow) {
-    // This depends on pseudo APIs of browser.js and tabbrowser.xml
-    aContentWindow = aContentWindow.top;
-    var browsers = aBrowserWindow.gBrowser.browsers;
-    for (let browser of browsers) {
-      if (browser.contentWindow == aContentWindow)
-        return browser;
-    }
-    // handle other browser/iframe elements that may need popupnotifications
-    let browser = aContentWindow
-                          .QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsIWebNavigation)
-                          .QueryInterface(Ci.nsIDocShell)
-                          .chromeEventHandler;
-    if (browser.getAttribute("popupnotificationanchor"))
-      return browser;
-    return null;
-  },
-
-  _getManifestURI: function(aWindow) {
-    if (!aWindow.document.documentElement)
-      return null;
-
-    var attr = aWindow.document.documentElement.getAttribute("manifest");
-    if (!attr)
-      return null;
-
-    try {
-      var contentURI = makeURI(aWindow.location.href, null, null);
-      return makeURI(attr, aWindow.document.characterSet, contentURI);
-    } catch (e) {
-      return null;
-    }
-  },
-
-  // A cache update isn't tied to a specific window.  Try to find
-  // the best browser in which to warn the user about space usage
-  _getBrowserForCacheUpdate: function(aCacheUpdate) {
-    // Prefer the current browser
-    var uri = this._getManifestURI(content);
-    if (uri && uri.equals(aCacheUpdate.manifestURI)) {
-      return gBrowser.selectedBrowser;
-    }
-
-    var browsers = gBrowser.browsers;
-    for (let browser of browsers) {
-      uri = this._getManifestURI(browser.contentWindow);
-      if (uri && uri.equals(aCacheUpdate.manifestURI)) {
-        return browser;
-      }
-    }
-
-    // is this from a non-tab browser/iframe?
-    browsers = document.querySelectorAll("iframe[popupnotificationanchor] | browser[popupnotificationanchor]");
-    for (let browser of browsers) {
-      uri = this._getManifestURI(browser.contentWindow);
-      if (uri && uri.equals(aCacheUpdate.manifestURI)) {
-        return browser;
-      }
-    }
-
-    return null;
-  },
-
-  _warnUsage: function(aBrowser, aURI) {
-    if (!aBrowser)
+  warnUsage(browser, uri) {
+    if (!browser)
       return;
 
     let mainAction = {
       label: gNavigatorBundle.getString("offlineApps.manageUsage"),
       accessKey: gNavigatorBundle.getString("offlineApps.manageUsageAccessKey"),
-      callback: OfflineApps.manage
+      callback: this.manage
     };
 
-    let warnQuota = gPrefService.getIntPref("offline-apps.quota.warn");
+    let warnQuota = Services.prefs.getIntPref("offline-apps.quota.warn");
     let message = gNavigatorBundle.getFormattedString("offlineApps.usage",
-                                                      [ aURI.host,
+                                                      [ uri.host,
                                                         warnQuota / 1024 ]);
 
     let anchorID = "indexedDB-notification-icon";
-    PopupNotifications.show(aBrowser, "offline-app-usage", message,
+    PopupNotifications.show(browser, "offline-app-usage", message,
                             anchorID, mainAction);
 
     // Now that we've warned once, prevent the warning from showing up
     // again.
-    Services.perms.add(aURI, "offline-app",
+    Services.perms.add(uri, "offline-app",
                        Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN);
   },
 
   // XXX: duplicated in preferences/advanced.js
-  _getOfflineAppUsage: function (host, groups)
-  {
-    var cacheService = Cc["@mozilla.org/network/application-cache-service;1"].
+  _getOfflineAppUsage(host) {
+    let cacheService = Cc["@mozilla.org/network/application-cache-service;1"].
                        getService(Ci.nsIApplicationCacheService);
-    if (!groups)
-      groups = cacheService.getGroups();
-
-    var usage = 0;
+    let groups = cacheService.getGroups();
+    let usage = 0;
     for (let group of groups) {
-      var uri = Services.io.newURI(group, null, null);
+      let uri = Services.io.newURI(group, null, null);
       if (uri.asciiHost == host) {
-        var cache = cacheService.getActiveCache(group);
+        let cache = cacheService.getActiveCache(group);
         usage += cache.usage;
       }
     }
 
     return usage;
   },
 
-  _checkUsage: function(aURI) {
+  _checkUsage(uri) {
     // if the user has already allowed excessive usage, don't bother checking
-    if (Services.perms.testExactPermission(aURI, "offline-app") !=
+    if (Services.perms.testExactPermission(uri, "offline-app") !=
         Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN) {
-      var usage = this._getOfflineAppUsage(aURI.asciiHost);
-      var warnQuota = gPrefService.getIntPref("offline-apps.quota.warn");
+      let usage = this._getOfflineAppUsage(uri.asciiHost);
+      let warnQuota = Services.prefs.getIntPref("offline-apps.quota.warn");
       if (usage >= warnQuota * 1024) {
         return true;
       }
     }
 
     return false;
   },
 
-  offlineAppRequested: function(aContentWindow) {
-    if (!gPrefService.getBoolPref("browser.offline-apps.notify")) {
-      return;
-    }
-
-    let browserWindow = this._getBrowserWindowForContentWindow(aContentWindow);
-    let browser = this._getBrowserForContentWindow(browserWindow,
-                                                   aContentWindow);
-
-    let currentURI = aContentWindow.document.documentURIObject;
-
-    // don't bother showing UI if the user has already made a decision
-    if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION)
-      return;
-
-    try {
-      if (gPrefService.getBoolPref("offline-apps.allow_by_default")) {
-        // all pages can use offline capabilities, no need to ask the user
-        return;
-      }
-    } catch(e) {
-      // this pref isn't set by default, ignore failures
-    }
-
-    let host = currentURI.asciiHost;
+  requestPermission(browser, docId, uri) {
+    let host = uri.asciiHost;
     let notificationID = "offline-app-requested-" + host;
     let notification = PopupNotifications.getNotification(notificationID, browser);
 
     if (notification) {
-      notification.options.documents.push(aContentWindow.document);
+      notification.options.controlledItems.push([
+        Cu.getWeakReference(browser), docId, uri
+      ]);
     } else {
       let mainAction = {
         label: gNavigatorBundle.getString("offlineApps.allow"),
         accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"),
         callback: function() {
-          for (let document of notification.options.documents) {
-            OfflineApps.allowSite(document);
+          for (let [browser, docId, uri] of notification.options.controlledItems) {
+            OfflineApps.allowSite(browser, docId, uri);
           }
         }
       };
       let secondaryActions = [{
         label: gNavigatorBundle.getString("offlineApps.never"),
         accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"),
         callback: function() {
-          for (let document of notification.options.documents) {
-            OfflineApps.disallowSite(document);
+          for (let [, , uri] of notification.options.controlledItems) {
+            OfflineApps.disallowSite(uri);
           }
         }
       }];
       let message = gNavigatorBundle.getFormattedString("offlineApps.available",
-                                                        [ host ]);
+                                                        [host]);
       let anchorID = "indexedDB-notification-icon";
-      let options= {
-        documents : [ aContentWindow.document ]
+      let options = {
+        controlledItems : [[Cu.getWeakReference(browser), docId, uri]]
       };
       notification = PopupNotifications.show(browser, notificationID, message,
                                              anchorID, mainAction,
                                              secondaryActions, options);
     }
   },
 
-  allowSite: function(aDocument) {
-    Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.ALLOW_ACTION);
+  disallowSite(uri) {
+    Services.perms.add(uri, "offline-app", Services.perms.DENY_ACTION);
+  },
+
+  allowSite(browserRef, docId, uri) {
+    Services.perms.add(uri, "offline-app", Services.perms.ALLOW_ACTION);
 
     // When a site is enabled while loading, manifest resources will
     // start fetching immediately.  This one time we need to do it
     // ourselves.
-    this._startFetching(aDocument);
-  },
-
-  disallowSite: function(aDocument) {
-    Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.DENY_ACTION);
-  },
-
-  manage: function() {
+    let browser = browserRef.get();
+    if (browser && browser.messageManager) {
+      browser.messageManager.sendAsyncMessage("OfflineApps:StartFetching", {
+        docId,
+      });
+    }
+  },
+
+  manage() {
     openAdvancedPreferences("networkTab");
   },
 
-  _startFetching: function(aDocument) {
-    if (!aDocument.documentElement)
-      return;
-
-    var manifest = aDocument.documentElement.getAttribute("manifest");
-    if (!manifest)
-      return;
-
-    var manifestURI = makeURI(manifest, aDocument.characterSet,
-                              aDocument.documentURIObject);
-
-    var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"].
-                        getService(Ci.nsIOfflineCacheUpdateService);
-    updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject,
-                                 aDocument.nodePrincipal, window);
-  },
-
-  /////////////////////////////////////////////////////////////////////////////
-  // nsIObserver
-  observe: function (aSubject, aTopic, aState)
-  {
-    if (aTopic == "offline-cache-update-completed") {
-      var cacheUpdate = aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate);
-
-      var uri = cacheUpdate.manifestURI;
-      if (OfflineApps._checkUsage(uri)) {
-        var browser = this._getBrowserForCacheUpdate(cacheUpdate);
-        if (browser) {
-          OfflineApps._warnUsage(browser, cacheUpdate.manifestURI);
+  receiveMessage(msg) {
+    switch (msg.name) {
+      case "OfflineApps:CheckUsage":
+        let uri = makeURI(msg.data.uri);
+        if (this._checkUsage(uri)) {
+          this.warnUsage(msg.target, uri);
         }
-      }
-    }
-  }
+        break;
+      case "OfflineApps:RequestPermission":
+        this.requestPermission(msg.target, msg.data.docId, makeURI(msg.data.uri));
+        break;
+    }
+  },
+
+  init() {
+    let mm = window.messageManager;
+    mm.addMessageListener("OfflineApps:CheckUsage", this);
+    mm.addMessageListener("OfflineApps:RequestPermission", this);
+  },
 };
 
 var IndexedDBPromptHelper = {
   _permissionsPrompt: "indexedDB-permissions-prompt",
   _permissionsResponse: "indexedDB-permissions-response",
 
   _notificationIcon: "indexedDB-notification-icon",
 
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -1313,8 +1313,115 @@ var PageInfoListener = {
     let middleRE = /\s+/g;
     let endRE = /(^\s+)|(\s+$)/g;
 
     text = text.replace(middleRE, " ");
     return text.replace(endRE, "");
   }
 };
 PageInfoListener.init();
+
+let OfflineApps = {
+  _docId: 0,
+  _docIdMap: new Map(),
+
+  _docManifestSet: new Set(),
+
+  _observerAdded: false,
+  registerWindow(aWindow) {
+    if (!this._observerAdded) {
+      this._observerAdded = true;
+      Services.obs.addObserver(this, "offline-cache-update-completed", true);
+    }
+    let manifestURI = this._getManifestURI(aWindow);
+    this._docManifestSet.add(manifestURI.spec);
+  },
+
+  handleEvent: function(event) {
+    if (event.type == "MozApplicationManifest") {
+      this.offlineAppRequested(event.originalTarget.defaultView);
+    }
+  },
+
+  _getManifestURI: function(aWindow) {
+    if (!aWindow.document.documentElement)
+      return null;
+
+    var attr = aWindow.document.documentElement.getAttribute("manifest");
+    if (!attr)
+      return null;
+
+    try {
+      var contentURI = BrowserUtils.makeURI(aWindow.location.href, null, null);
+      return BrowserUtils.makeURI(attr, aWindow.document.characterSet, contentURI);
+    } catch (e) {
+      return null;
+    }
+  },
+
+  offlineAppRequested: function(aContentWindow) {
+    this.registerWindow(aContentWindow);
+    if (!Services.prefs.getBoolPref("browser.offline-apps.notify")) {
+      return;
+    }
+
+    let currentURI = aContentWindow.document.documentURIObject;
+    // don't bother showing UI if the user has already made a decision
+    if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION)
+      return;
+
+    try {
+      if (Services.prefs.getBoolPref("offline-apps.allow_by_default")) {
+        // all pages can use offline capabilities, no need to ask the user
+        return;
+      }
+    } catch(e) {
+      // this pref isn't set by default, ignore failures
+    }
+    let docId = ++this._docId;
+    this._docIdMap.set(docId, Cu.getWeakReference(aContentWindow.document));
+    sendAsyncMessage("OfflineApps:RequestPermission", {
+      uri: currentURI.spec,
+      docId,
+    });
+  },
+
+  _startFetching: function(aDocument) {
+    if (!aDocument.documentElement)
+      return;
+
+    let manifestURI = this._getManifestURI(aDocument.defaultView);
+    if (!manifestURI)
+      return;
+
+    var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"].
+                        getService(Ci.nsIOfflineCacheUpdateService);
+    updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject,
+                                 aDocument.nodePrincipal, aDocument.defaultView);
+  },
+
+  receiveMessage(aMessage) {
+    if (aMessage.name == "OfflineApps:StartFetching") {
+      let doc = this._docIdMap.get(aMessage.data.docId);
+      doc = doc && doc.get();
+      if (doc) {
+        this._startFetching(doc);
+      }
+      this._docIdMap.delete(aMessage.data.docId);
+    }
+  },
+
+  observe(aSubject, aTopic, aState) {
+    if (aTopic == "offline-cache-update-completed") {
+      let cacheUpdate = aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate);
+      let uri = cacheUpdate.manifestURI;
+      if (uri && this._docManifestSet.has(uri.spec)) {
+        sendAsyncMessage("OfflineApps:CheckUsage", {uri: uri.spec});
+      }
+    }
+  },
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+};
+
+addEventListener("MozApplicationManifest", OfflineApps, false);
+addMessageListener("OfflineApps:StartFetching", OfflineApps);
+
--- a/browser/base/content/test/general/browser_offlineQuotaNotification.js
+++ b/browser/base/content/test/general/browser_offlineQuotaNotification.js
@@ -10,16 +10,18 @@ const URL = "http://mochi.test:8888/brow
 
 registerCleanupFunction(function() {
   // Clean up after ourself
   let uri = Services.io.newURI(URL, null, null);
   let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
   Services.perms.removeFromPrincipal(principal, "offline-app");
   Services.prefs.clearUserPref("offline-apps.quota.warn");
   Services.prefs.clearUserPref("offline-apps.allow_by_default");
+  let {OfflineAppCacheHelper} = Components.utils.import("resource:///modules/offlineAppCache.jsm", {});
+  OfflineAppCacheHelper.clear();
 });
 
 // Same as the other one, but for in-content preferences
 function checkInContentPreferences(win) {
   let doc = win.document;
   let sel = doc.getElementById("categories").selectedItems[0].id;
   let tab = doc.getElementById("advancedPrefs").selectedTab.id;
   is(gBrowser.currentURI.spec, "about:preferences#advanced", "about:preferences loaded");
@@ -39,40 +41,58 @@ function test() {
   gBrowser.selectedTab = gBrowser.addTab(URL);
   registerCleanupFunction(() => gBrowser.removeCurrentTab());
 
 
   Promise.all([
     // Wait for a notification that asks whether to allow offline storage.
     promiseNotification(),
     // Wait for the tab to load.
-    BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser)
+    BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser),
   ]).then(() => {
-    gBrowser.selectedBrowser.contentWindow.applicationCache.oncached = function() {
-      executeSoon(function() {
-        // We got cached - now we should have provoked the quota warning.
-        let notification = PopupNotifications.getNotification('offline-app-usage');
-        ok(notification, "have offline-app-usage notification");
-        // select the default action - this should cause the preferences
-        // tab to open - which we track via an "Initialized" event.
-        PopupNotifications.panel.firstElementChild.button.click();
-        let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
-        newTabBrowser.addEventListener("Initialized", function PrefInit() {
-          newTabBrowser.removeEventListener("Initialized", PrefInit, true);
-          executeSoon(function() {
-            checkInContentPreferences(newTabBrowser.contentWindow);
-          })
-        }, true);
+    info("Loaded page, adding onCached handler");
+    // Need a promise to keep track of when we've added our handler.
+    let onCachedAttached = new Promise(resolve => {
+      let onCachedAttached = msg => {
+        mm.removeMessageListener("Test:OnCachedAttached", onCachedAttached);
+        resolve();
+      };
+      let mm = gBrowser.selectedBrowser.messageManager;
+      mm.addMessageListener("Test:OnCachedAttached", onCachedAttached);
+    });
+    let gotCached = ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+      return new Promise(resolve => {
+        content.window.applicationCache.oncached = function() {
+          setTimeout(resolve, 0);
+        };
+        sendAsyncMessage("Test:OnCachedAttached");
       });
-    };
-    Services.prefs.setIntPref("offline-apps.quota.warn", 1);
+    });
+    gotCached.then(function() {
+      // We got cached - now we should have provoked the quota warning.
+      let notification = PopupNotifications.getNotification('offline-app-usage');
+      ok(notification, "have offline-app-usage notification");
+      // select the default action - this should cause the preferences
+      // tab to open - which we track via an "Initialized" event.
+      PopupNotifications.panel.firstElementChild.button.click();
+      let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+      newTabBrowser.addEventListener("Initialized", function PrefInit() {
+        newTabBrowser.removeEventListener("Initialized", PrefInit, true);
+        executeSoon(function() {
+          checkInContentPreferences(newTabBrowser.contentWindow);
+        })
+      }, true);
+    });
+    onCachedAttached.then(function() {
+      Services.prefs.setIntPref("offline-apps.quota.warn", 1);
 
-    // Click the notification panel's "Allow" button.  This should kick
-    // off updates which will call our oncached handler above.
-    PopupNotifications.panel.firstElementChild.button.click();
+      // Click the notification panel's "Allow" button.  This should kick
+      // off updates which will call our oncached handler above.
+      PopupNotifications.panel.firstElementChild.button.click();
+    });
   });
 }
 
 function promiseNotification() {
   return new Promise(resolve => {
     PopupNotifications.panel.addEventListener("popupshown", function onShown() {
       PopupNotifications.panel.removeEventListener("popupshown", onShown);
       resolve();
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -1125,8 +1125,20 @@ var ViewSelectionSource = {
     // replace chars > 0x7f via nsIEntityConverter
     str = str.replace(/[^\0-\u007f]/g, convertEntity);
 
     return str;
   }
 };
 
 ViewSelectionSource.init();
+
+addEventListener("MozApplicationManifest", function(e) {
+  let doc = e.target;
+  let info = {
+    uri: doc.documentURI,
+    characterSet: doc.characterSet,
+    manifest: doc.documentElement.getAttribute("manifest"),
+    principal: doc.nodePrincipal,
+  };
+  sendAsyncMessage("MozApplicationManifest", info);
+}, false);
+
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -493,16 +493,23 @@
                 readonly="true"/>
 
       <property name="showWindowResizer"
                 onset="if (val) this.setAttribute('showresizer', 'true');
                        else this.removeAttribute('showresizer');
                        return val;"
                 onget="return this.getAttribute('showresizer') == 'true';"/>
 
+      <property name="manifestURI"
+                readonly="true">
+        <getter><![CDATA[
+          return this.contentDocument.documentElement &&
+                 this.contentDocument.documentElement.getAttribute("manifest");
+        ]]></getter>
+      </property>
 
       <property name="fullZoom">
         <getter><![CDATA[
           return this.markupDocumentViewer.fullZoom;
         ]]></getter>
         <setter><![CDATA[
           this.markupDocumentViewer.fullZoom = val;
         ]]></setter>
@@ -1214,16 +1221,17 @@
               "_contentTitle",
               "_characterSet",
               "_contentPrincipal",
               "_imageDocument",
               "_fullZoom",
               "_textZoom",
               "_isSyntheticDocument",
               "_innerWindowID",
+              "_manifestURI",
             ]);
           }
 
           var ourFieldValues = {};
           var otherFieldValues = {};
           for (let field of fieldsToSwap) {
             ourFieldValues[field] = this[field];
             otherFieldValues[field] = aOtherBrowser[field];
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -254,16 +254,21 @@
         <body><![CDATA[
           // See the explanation for what this does in nsITabParent.ipdl
 
           let {frameLoader} = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner);
           frameLoader.tabParent.setDocShellIsActiveAndForeground(isActive);
         ]]></body>
       </method>
 
+      <field name="_manifestURI"/>
+      <property name="manifestURI"
+                onget="return this._manifestURI"
+                readonly="true"/>
+
       <field name="mDestroyed">false</field>
 
       <field name="_permitUnloadId">0</field>
 
       <method name="getInPermitUnload">
         <parameter name="aCallback"/>
         <body>
         <![CDATA[
@@ -358,16 +363,17 @@
           this.messageManager.addMessageListener("DOMTitleChanged", this);
           this.messageManager.addMessageListener("ImageDocumentLoaded", this);
           this.messageManager.addMessageListener("DocumentInserted", this);
           this.messageManager.addMessageListener("FullZoomChange", this);
           this.messageManager.addMessageListener("TextZoomChange", this);
           this.messageManager.addMessageListener("ZoomChangeUsingMouseWheel", this);
           this.messageManager.addMessageListener("DOMFullscreen:RequestExit", this);
           this.messageManager.addMessageListener("DOMFullscreen:RequestRollback", this);
+          this.messageManager.addMessageListener("MozApplicationManifest", this);
           this.messageManager.loadFrameScript("chrome://global/content/browser-child.js", true);
 
           if (this.hasAttribute("selectmenulist")) {
             this.messageManager.addMessageListener("Forms:ShowDropDown", this);
             this.messageManager.addMessageListener("Forms:HideDropDown", this);
             this.messageManager.loadFrameScript("chrome://global/content/select-child.js", true);
           }
 
@@ -492,16 +498,20 @@
 
             case "DOMFullscreen:RequestRollback": {
               let windowUtils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                                       .getInterface(Components.interfaces.nsIDOMWindowUtils);
               windowUtils.remoteFrameFullscreenReverted();
               break;
             }
 
+            case "MozApplicationManifest":
+              this._manifestURI = aMessage.data.manifest;
+              break;
+
             default:
               // Delegate to browser.xml.
               return this._receiveMessage(aMessage);
               break;
           }
         ]]></body>
       </method>