Bug 1378647 - support creating lazy tabs from extensions, r?rpl draft
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 28 Jun 2018 10:14:40 -0400
changeset 812000 fd6e4dac15114cd9fea4604306adad2dc766beb7
parent 811302 9c7bb8874337c2d40aef3d9945b10490a5115188
push id114449
push usermixedpuppy@gmail.com
push dateThu, 28 Jun 2018 14:18:51 +0000
reviewersrpl
bugs1378647
milestone63.0a1
Bug 1378647 - support creating lazy tabs from extensions, r?rpl MozReview-Commit-ID: 9QMkNtCQG6P
browser/components/extensions/parent/ext-tabs.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
--- a/browser/components/extensions/parent/ext-tabs.js
+++ b/browser/components/extensions/parent/ext-tabs.js
@@ -9,16 +9,18 @@ ChromeUtils.defineModuleGetter(this, "Ex
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                                "resource://gre/modules/PrivateBrowsingUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "PromiseUtils",
                                "resource://gre/modules/PromiseUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "Services",
                                "resource://gre/modules/Services.jsm");
 ChromeUtils.defineModuleGetter(this, "SessionStore",
                                "resource:///modules/sessionstore/SessionStore.jsm");
+ChromeUtils.defineModuleGetter(this, "Utils",
+                               "resource://gre/modules/sessionstore/Utils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
   return Services.strings.createBundle("chrome://global/locale/extensions.properties");
 });
 
 var {
   ExtensionError,
 } = ExtensionUtils;
@@ -554,45 +556,76 @@ this.tabs = class extends ExtensionAPI {
                 if (!containerId) {
                   return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
                 }
 
                 options.userContextId = containerId;
               }
             }
 
-            // Make sure things like about:blank and data: URIs never inherit,
-            // and instead always get a NullPrincipal.
-            options.disallowInheritPrincipal = true;
+            // Only set disallowInheritPrincipal on non-discardable urls as it
+            // will override creating a lazy browser.  Setting triggeringPrincipal
+            // will ensure other cases are handled, but setting it may prevent
+            // creating about and data urls.
+            let discardable = url && !url.startsWith("about:") && !url.startsWith("data:");
+            if (!discardable) {
+              // Make sure things like about:blank and data: URIs never inherit,
+              // and instead always get a NullPrincipal.
+              options.disallowInheritPrincipal = true;
+            } else {
+              options.triggeringPrincipal = context.principal;
+            }
 
             tabListener.initTabReady();
             let currentTab = window.gBrowser.selectedTab;
 
             if (createProperties.openerTabId !== null) {
               options.ownerTab = tabTracker.getTab(createProperties.openerTabId);
               options.openerBrowser = options.ownerTab.linkedBrowser;
               if (options.ownerTab.ownerGlobal !== window) {
                 return Promise.reject({message: "Opener tab must be in the same window as the tab being created"});
               }
             }
 
-            if (createProperties.index != null) {
-              options.index = createProperties.index;
+            // Simple properties
+            const properties = ["index", "pinned", "title"];
+            for (let prop of properties) {
+              if (createProperties[prop] != null) {
+                options[prop] = createProperties[prop];
+              }
             }
 
-            if (createProperties.pinned != null) {
-              options.pinned = createProperties.pinned;
-            }
-
-            let nativeTab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
-
             let active = true;
             if (createProperties.active !== null) {
               active = createProperties.active;
             }
+            if (createProperties.discarded) {
+              if (active) {
+                return Promise.reject({message: `Active tabs cannot be created and discarded.`});
+              }
+              if (createProperties.pinned) {
+                return Promise.reject({message: `Pinned tabs cannot be created and discarded.`});
+              }
+              if (!discardable) {
+                return Promise.reject({message: `Cannot create a discarded new tab, about or data urls.`});
+              }
+              options.createLazyBrowser = true;
+            }
+
+            let nativeTab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
+            if (createProperties.discarded) {
+              SessionStore.setTabState(nativeTab, JSON.stringify({
+                entries: [{
+                  url: url,
+                  title: options.title,
+                  triggeringPrincipal_base64: Utils.serializePrincipal(context.principal),
+                }],
+              }));
+            }
+
             if (active) {
               window.gBrowser.selectedTab = nativeTab;
             }
 
             if (active && !url) {
               window.focusAndSelectUrlBar();
             }
 
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -551,16 +551,17 @@
               "url": {
                 "type": "string",
                 "optional": true,
                 "description": "The URL to navigate the tab to initially. Fully-qualified URLs must include a scheme (i.e. 'http://www.google.com', not 'www.google.com'). Relative URLs will be relative to the current page within the extension. Defaults to the New Tab Page."
               },
               "active": {
                 "type": "boolean",
                 "optional": true,
+                "default": true,
                 "description": "Whether the tab should become the active tab in the window. Does not affect whether the window is focused (see $(ref:windows.update)). Defaults to <var>true</var>."
               },
               "selected": {
                 "deprecated": "Please use <em>active</em>.",
                 "unsupported": true,
                 "type": "boolean",
                 "optional": true,
                 "description": "Whether the tab should become the selected tab in the window. Defaults to <var>true</var>"
@@ -578,17 +579,29 @@
               },
               "cookieStoreId": {
                 "type": "string",
                 "optional": true,
                 "description": "The CookieStoreId for the tab that opened this tab."
               },
               "openInReaderMode": {
                 "type": "boolean",
-                "optional": true, "description": "Whether the document in the tab should be opened in reader mode."}
+                "optional": true,
+                "description": "Whether the document in the tab should be opened in reader mode."
+              },
+              "discarded": {
+                "type": "boolean",
+                "optional": true,
+                "description": "Whether the tab is discarded when created."
+              },
+              "title": {
+                "type": "string",
+                "optional": true,
+                "description": "The title used for display if the tab is created in discarded mode."
+              }
             }
           },
           {
             "type": "function",
             "name": "callback",
             "optional": true,
             "parameters": [
               {
--- a/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
@@ -6,70 +6,43 @@
 const {Utils} = ChromeUtils.import("resource://gre/modules/sessionstore/Utils.jsm", {});
 const triggeringPrincipal_base64 = Utils.SERIALIZED_SYSTEMPRINCIPAL;
 
 let lazyTabState = {entries: [{url: "http://example.com/", triggeringPrincipal_base64, title: "Example Domain"}]};
 
 add_task(async function test_discarded() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
-      "permissions": ["tabs"],
+      "permissions": ["tabs", "webNavigation"],
     },
 
     background: async function() {
-      let onCreatedTabData = [];
-      let discardedEventData = [];
-
-      async function finishTest() {
-        browser.test.assertEq(0, discardedEventData.length, "number of discarded events fired");
-
-        onCreatedTabData.sort((data1, data2) => data1.index - data2.index);
-        browser.test.assertEq(false, onCreatedTabData[0].discarded, "non-lazy tab onCreated discard property");
-        browser.test.assertEq(true, onCreatedTabData[1].discarded, "lazy tab onCreated discard property");
-
-        let tabs = await browser.tabs.query({currentWindow: true});
-        tabs.sort((tab1, tab2) => tab1.index - tab2.index);
-
-        browser.test.assertEq(false, tabs[1].discarded, "non-lazy tab query discard property");
-        browser.test.assertEq(true, tabs[2].discarded, "lazy tab query discard property");
-
-        let updatedTab = await browser.tabs.update(tabs[2].id, {active: true});
+      browser.webNavigation.onCompleted.addListener(async (details) => {
+        browser.test.log(`webNav onCompleted received for ${details.tabId}`);
+        let updatedTab = await browser.tabs.get(details.tabId);
         browser.test.assertEq(false, updatedTab.discarded, "lazy to non-lazy update discard property");
-        browser.test.assertEq(false, discardedEventData[0], "lazy to non-lazy onUpdated discard property");
-
         browser.test.notifyPass("test-finished");
-      }
-
-      browser.tabs.onUpdated.addListener(function(tabId, updatedInfo) {
-        if ("discarded" in updatedInfo) {
-          discardedEventData.push(updatedInfo.discarded);
-        }
-      });
+      }, {url: [{hostContains: "example.com"}]});
 
       browser.tabs.onCreated.addListener(function(tab) {
-        onCreatedTabData.push({discarded: tab.discarded, index: tab.index});
-        if (onCreatedTabData.length == 2) {
-          finishTest();
-        }
+        browser.test.assertEq(true, tab.discarded, "non-lazy tab onCreated discard property");
+        browser.tabs.update(tab.id, {active: true});
       });
     },
   });
 
   await extension.startup();
 
-  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
-
-  let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank", {createLazyBrowser: true});
-  SessionStore.setTabState(tab2, JSON.stringify(lazyTabState));
+  let testTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {createLazyBrowser: true});
+  SessionStore.setTabState(testTab, JSON.stringify(lazyTabState));
 
   await extension.awaitFinish("test-finished");
   await extension.unload();
 
-  BrowserTestUtils.removeTab(tab1);
-  BrowserTestUtils.removeTab(tab2);
+  BrowserTestUtils.removeTab(testTab);
 });
 
 // If discard is called immediately after creating a new tab, the new tab may not have loaded,
 // and the sessionstore for that tab is not ready for discarding.  The result was a corrupted
 // sessionstore for the tab, which when the tab was activated, resulted in a tab with partial
 // state.
 add_task(async function test_create_then_discard() {
   let extension = ExtensionTestUtils.loadExtension({
@@ -100,8 +73,44 @@ add_task(async function test_create_then
       createdTab = await browser.tabs.create({url: "http://example.com/", active: false});
       browser.tabs.discard(createdTab.id);
     },
   });
   await extension.startup();
   await extension.awaitFinish("test-finished");
   await extension.unload();
 });
+
+add_task(async function test_create_discarded() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["tabs", "webNavigation"],
+    },
+
+    background: async function() {
+      let tabOpts = {
+        url: "http://example.com/",
+        active: false,
+        discarded: true,
+        title: "discarded tab",
+      };
+
+      browser.webNavigation.onCompleted.addListener(async (details) => {
+        let activeTab = await browser.tabs.get(details.tabId);
+        browser.test.assertEq(tabOpts.url, activeTab.url, "restored tab url matches active tab url");
+        browser.test.assertEq("mochitest index /", activeTab.title, "restored tab title is correct");
+        browser.tabs.remove(details.tabId);
+        browser.test.notifyPass("test-finished");
+      }, {url: [{hostContains: "example.com"}]});
+
+      browser.tabs.onCreated.addListener(tab => {
+        browser.test.assertEq(tabOpts.url, tab.url, "lazy tab url is correct");
+        browser.test.assertEq(tabOpts.title, tab.title, "lazy tab title is correct");
+        browser.tabs.update(tab.id, {active: true});
+      });
+
+      browser.tabs.create(tabOpts);
+    },
+  });
+  await extension.startup();
+  await extension.awaitFinish("test-finished");
+  await extension.unload();
+});