Bug 1369209 - Implement management.install for themes only r=kmag,robwu
☠☠ backed out by 15a645ce3c64 ☠ ☠
authorTomislav Jovanovic <tomica@gmail.com>
Thu, 23 Aug 2018 18:45:23 +0000
changeset 488252 29cdfd4dec2329d15b97543e8189073ef99aa6c7
parent 488251 1eb34707779fdb6acecf803904abddb64593f57b
child 488253 f7f9e47f0608238b968458cd77d2d9a4de7b9b51
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag, robwu
bugs1369209
milestone63.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 1369209 - Implement management.install for themes only r=kmag,robwu Differential Revision: https://phabricator.services.mozilla.com/D3106
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_management.js
browser/components/extensions/test/browser/install_other-1.0-fx.xpi
browser/components/extensions/test/browser/install_theme-1.0-fx.xpi
toolkit/components/extensions/parent/ext-management.js
toolkit/components/extensions/schemas/management.json
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -23,16 +23,18 @@ support-files =
   file_language_fr_en.html
   file_language_ja.html
   file_language_tlh.html
   file_dummy.html
   file_title.html
   file_inspectedwindow_reload_target.sjs
   file_indexedDB.html
   file_serviceWorker.html
+  install_other-1.0-fx.xpi
+  install_theme-1.0-fx.xpi
   webNav_createdTarget.html
   webNav_createdTargetSource.html
   webNav_createdTargetSource_subframe.html
   serviceWorker.js
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
   ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js
   ../../../../../toolkit/components/extensions/test/mochitest/redirection.sjs
@@ -100,16 +102,17 @@ support-files =
 [browser_ext_find.js]
 skip-if = (verify && (os == 'linux' || os == 'mac'))
 [browser_ext_getViews.js]
 [browser_ext_history_redirect.js]
 [browser_ext_identity_indication.js]
 [browser_ext_incognito_views.js]
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
+[browser_ext_management.js]
 [browser_ext_menus.js]
 [browser_ext_menus_accesskey.js]
 [browser_ext_menus_activeTab.js]
 [browser_ext_menus_errors.js]
 [browser_ext_menus_event_order.js]
 [browser_ext_menus_events.js]
 [browser_ext_menus_refresh.js]
 [browser_ext_menus_targetElement.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_management.js
@@ -0,0 +1,80 @@
+"use strict";
+
+const BASE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+
+function waitForTransition(element, propertyName) {
+  return BrowserTestUtils.waitForEvent(element, "transitionend", false, event => {
+    return event.target == element && event.propertyName == propertyName;
+  });
+}
+
+add_task(async function test_management_install() {
+  await SpecialPowers.pushPrefEnv({set: [
+    ["xpinstall.signatures.required", false],
+  ]});
+
+  registerCleanupFunction(async () => {
+    await SpecialPowers.popPrefEnv();
+  });
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      browser_action: {
+        "browser_style": false,
+      },
+      permissions: ["management"],
+    },
+    background() {
+      let addons;
+      browser.test.onMessage.addListener((msg, init) => {
+        addons = init;
+        browser.test.sendMessage("ready");
+      });
+      browser.browserAction.onClicked.addListener(async () => {
+        try {
+          let {url, hash} = addons.shift();
+          browser.test.log(`Installing XPI from ${url} with hash ${hash || "missing"}`);
+          let {id} = await browser.management.install({url, hash});
+          let {type} = await browser.management.get(id);
+          browser.test.sendMessage("installed", {id, type});
+        } catch (e) {
+          browser.test.log(`management.install() throws ${e}`);
+          browser.test.sendMessage("failed", e.message);
+        }
+      });
+    },
+  });
+
+  let addons = [{
+    url: BASE + "install_theme-1.0-fx.xpi",
+    hash: "sha256:aa232d8391d82a9c1014364efbe1657ff6d8dfc88b3c71e99881b1f3843fdad3",
+  }, {
+    url: BASE + "install_other-1.0-fx.xpi",
+  }];
+
+  await extension.startup();
+  extension.sendMessage("addons", addons);
+  await extension.awaitMessage("ready");
+
+  // Test installing a static WE theme.
+  let transitionDone = waitForTransition(document.documentElement, "background-color");
+  clickBrowserAction(extension);
+
+  let {id, type} = await extension.awaitMessage("installed");
+  is(id, "tiger@persona.beard", "Static web extension theme installed");
+  is(type, "theme", "Extension type is correct");
+
+  await transitionDone;
+  let style = window.getComputedStyle(document.documentElement);
+  is(style.backgroundColor, "rgb(255, 165, 0)", "Background is the new black");
+
+  let addon = await AddonManager.getAddonByID("tiger@persona.beard");
+  await addon.uninstall();
+
+  // Test installing a standard WE.
+  clickBrowserAction(extension);
+  let error = await extension.awaitMessage("failed");
+  is(error, "Incompatible addon", "Standard web extension rejected");
+
+  await extension.unload();
+});
new file mode 100644
new file mode 100644
--- a/toolkit/components/extensions/parent/ext-management.js
+++ b/toolkit/components/extensions/parent/ext-management.js
@@ -182,16 +182,38 @@ this.management = class extends Extensio
           let addons = await AddonManager.getAddonsByTypes(allowedTypes);
           return addons.filter(checkAllowedAddon).map(addon => {
             // If the extension is enabled get it and use it for more data.
             let ext = GlobalManager.extensionMap.get(addon.id);
             return getExtensionInfoForAddon(ext, addon);
           });
         },
 
+        async install({url, hash}) {
+          let listener = {
+            onDownloadEnded(install) {
+              if (install.addon.appDisabled || install.addon.type !== "theme") {
+                install.cancel();
+                return false;
+              }
+            },
+          };
+
+          let install = await AddonManager.getInstallForURL(url, "application/x-xpinstall", hash);
+          install.addListener(listener);
+          try {
+            await install.install();
+          } catch (e) {
+            Cu.reportError(e);
+            throw new ExtensionError("Incompatible addon");
+          }
+          await install.addon.enable();
+          return {id: install.addon.id};
+        },
+
         async getSelf() {
           let addon = await AddonManager.getAddonByID(extension.id);
           return getExtensionInfoForAddon(extension, addon);
         },
 
         async uninstallSelf(options) {
           if (options && options.showConfirmDialog) {
             let message = _("uninstall.confirmation.message", extension.name);
--- a/toolkit/components/extensions/schemas/management.json
+++ b/toolkit/components/extensions/schemas/management.json
@@ -39,17 +39,17 @@
       {
         "id": "ExtensionDisabledReason",
         "description": "A reason the item is disabled.",
         "type": "string",
         "enum": ["unknown", "permissions_increase"]
       },
       {
         "id": "ExtensionType",
-        "description": "The type of this extension. Will always be 'extension'.",
+        "description": "The type of this extension, 'extension' or 'theme'.",
         "type": "string",
         "enum": ["extension", "theme"]
       },
       {
         "id": "ExtensionInstallType",
         "description": "How the extension was installed. One of<br><var>development</var>: The extension was loaded unpacked in developer mode,<br><var>normal</var>: The extension was installed normally via an .xpi file,<br><var>sideload</var>: The extension was installed by other software on the machine,<br><var>other</var>: The extension was installed by other means.",
         "type": "string",
         "enum": ["development", "normal", "sideload", "other"]
@@ -94,17 +94,17 @@
             "type": "boolean"
           },
           "disabledReason": {
             "description": "A reason the item is disabled.",
             "$ref": "ExtensionDisabledReason",
             "optional": true
           },
           "type": {
-            "description": "The type of this extension. Will always return 'extension'.",
+            "description": "The type of this extension, 'extension' or 'theme'.",
             "$ref": "ExtensionType"
           },
           "homepageUrl": {
             "description": "The URL of the homepage of this extension.",
             "type": "string",
             "optional": true
           },
           "updateUrl": {
@@ -192,16 +192,58 @@
                 "name": "result",
                 "$ref": "ExtensionInfo"
               }
             ]
           }
         ]
       },
       {
+        "name": "install",
+        "type": "function",
+        "requireUserInput": true,
+        "permissions": ["management"],
+        "description": "Installs and enables a theme extension from the given url.",
+        "async": "callback",
+        "parameters": [
+          {
+            "name": "options",
+            "type": "object",
+            "properties": {
+              "url": {
+                "$ref": "manifest.HttpURL",
+                "description": "URL pointing to the XPI file on addons.mozilla.org or similar."
+              },
+              "hash": {
+                "type": "string",
+                "optional": true,
+                "pattern": "^(sha256|sha512):[0-9a-fA-F]{64,128}$",
+                "description": "A hash of the XPI file, using sha256 or stronger."
+              }
+            }
+          },
+          {
+            "name": "callback",
+            "type": "function",
+            "optional": true,
+            "parameters": [
+              {
+                "name": "result",
+                "type": "object",
+                "properties": {
+                  "id": {
+                    "$ref": "manifest.ExtensionID"
+                  }
+                }
+              }
+            ]
+          }
+        ]
+      },
+      {
         "name": "getSelf",
         "type": "function",
         "description": "Returns information about the calling extension. Note: This function can be used without requesting the 'management' permission in the manifest.",
         "async": "callback",
         "parameters": [
           {
             "type": "function",
             "name": "callback",