Bug 1498343 - Make it easier to recognize an extension that have access to the userScript.setScriptAPIs API method. r=mixedpuppy
authorLuca Greco <lgreco@mozilla.com>
Wed, 17 Oct 2018 12:39:20 +0000
changeset 497941 681ffcabae79527e14f5f795aaf360eeb2e85399
parent 497940 a0aa0b0aa078f7135e7d7032687e4a38e6c378f8
child 497942 fb372f5f22b3a8813b3c687e5a8d4ce3177fbdea
push id10016
push userrgurzau@mozilla.com
push dateMon, 22 Oct 2018 13:58:55 +0000
treeherdermozilla-beta@848cf6354160 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1498343
milestone64.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 1498343 - Make it easier to recognize an extension that have access to the userScript.setScriptAPIs API method. r=mixedpuppy Depends on D8538 Differential Revision: https://phabricator.services.mozilla.com/D8539
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/schemas/user_scripts_content.json
toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js
toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
toolkit/components/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -854,19 +854,30 @@ class ExtensionData {
 
     this.webAccessibleResources = manifestData.webAccessibleResources.map(res => new MatchGlob(res));
     this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions, {restrictSchemes: !this.hasPermission("mozillaAddons")});
 
     return this.manifest;
   }
 
   hasPermission(perm, includeOptional = false) {
+    // If the permission is a "manifest property" permission, we check if the extension
+    // does have the required property in its manifest.
     let manifest_ = "manifest:";
     if (perm.startsWith(manifest_)) {
-      return this.manifest[perm.substr(manifest_.length)] != null;
+      // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested").
+      let value = this.manifest;
+      for (let prop of perm.substr(manifest_.length).split(".")) {
+        if (!value) {
+          break;
+        }
+        value = value[prop];
+      }
+
+      return value != null;
     }
 
     if (this.permissions.has(perm)) {
       return true;
     }
 
     if (includeOptional && this.manifest.optional_permissions.includes(perm)) {
       return true;
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -768,19 +768,30 @@ class BrowserExtensionContent extends Ev
     return this.localeData.localizeMessage(...args);
   }
 
   localize(...args) {
     return this.localeData.localize(...args);
   }
 
   hasPermission(perm) {
-    let match = /^manifest:(.*)/.exec(perm);
-    if (match) {
-      return this.manifest[match[1]] != null;
+    // If the permission is a "manifest property" permission, we check if the extension
+    // does have the required property in its manifest.
+    let manifest_ = "manifest:";
+    if (perm.startsWith(manifest_)) {
+      // Handle nested "manifest property" permission (e.g. as in "manifest:property.nested").
+      let value = this.manifest;
+      for (let prop of perm.substr(manifest_.length).split(".")) {
+        if (!value) {
+          break;
+        }
+        value = value[prop];
+      }
+
+      return value != null;
     }
     return this.permissions.has(perm);
   }
 }
 
 /**
  * An object that runs an remote implementation of an API.
  */
--- a/toolkit/components/extensions/schemas/user_scripts_content.json
+++ b/toolkit/components/extensions/schemas/user_scripts_content.json
@@ -13,16 +13,17 @@
         "type": "object",
         "description": "A set of API methods provided by the extensions to its userScripts",
         "additionalProperties": { "type": "function" }
       }
     ],
     "functions": [
       {
         "name": "setScriptAPIs",
+        "permissions": ["manifest:user_scripts.api_script"],
         "allowedContexts": ["content", "content_only"],
         "type": "function",
         "description": "Provides a set of custom API methods available to the registered userScripts",
         "parameters": [
           {
             "name": "exportedAPIMethods",
             "$ref": "ExportedAPIMethods"
           }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_manifest_permissions.js
@@ -0,0 +1,128 @@
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+const {ExtensionAPI} = ExtensionCommon;
+
+Cu.importGlobalProperties(["Blob", "URL"]);
+
+add_task(async function() {
+  const schema = [
+    {
+      namespace: "manifest",
+      types: [
+        {
+          "$extend": "WebExtensionManifest",
+          properties: {
+            a_manifest_property: {
+              type: "object",
+              optional: true,
+              properties: {
+                nested: {
+                  optional: true,
+                  type: "any",
+                },
+              },
+              additionalProperties: {"$ref": "UnrecognizedProperty"},
+            },
+          },
+        },
+      ],
+    },
+    {
+      namespace: "testManifestPermission",
+      permissions: ["manifest:a_manifest_property"],
+      functions: [
+        {
+          name: "testMethod",
+          type: "function",
+          async: true,
+          parameters: [],
+          permissions: ["manifest:a_manifest_property.nested"],
+        },
+      ],
+    },
+  ];
+
+  class FakeAPI extends ExtensionAPI {
+    getAPI(context) {
+      return {
+        testManifestPermission: {
+          get testProperty() {
+            return "value";
+          },
+          testMethod() {
+            return Promise.resolve("value");
+          },
+        },
+      };
+    }
+  }
+
+  const modules = {
+    testNamespace: {
+      url: URL.createObjectURL(new Blob([FakeAPI.toString()])),
+      schema: `data:,${JSON.stringify(schema)}`,
+      scopes: ["addon_parent", "addon_child"],
+      paths: [["testManifestPermission"]],
+    },
+  };
+
+  Services.catMan.addCategoryEntry("webextension-modules", "test-manifest-permission",
+                                   `data:,${JSON.stringify(modules)}`, false, false);
+
+  async function testExtension(extensionDef, assertFn) {
+    let extension = ExtensionTestUtils.loadExtension(extensionDef);
+
+    await extension.startup();
+    await assertFn(extension);
+    await extension.unload();
+  }
+
+  await testExtension({
+    manifest: {
+      a_manifest_property: {},
+    },
+    background() {
+      // Test hasPermission method implemented in ExtensionChild.jsm.
+      browser.test.assertTrue("testManifestPermission" in browser, "The API namespace is defined as expected");
+      browser.test.assertEq(
+        undefined, browser.testManifestPermission && browser.testManifestPermission.testMethod,
+        "The property with nested manifest property permission should not be available ");
+      browser.test.notifyPass("test-extension-manifest-without-nested-prop");
+    },
+  }, async (extension) => {
+    await extension.awaitFinish("test-extension-manifest-without-nested-prop");
+
+    // Test hasPermission method implemented in Extension.jsm.
+    equal(extension.extension.hasPermission("manifest:a_manifest_property"), true,
+          "Got the expected Extension's hasPermission result on existing property");
+    equal(extension.extension.hasPermission("manifest:a_manifest_property.nested"), false,
+          "Got the expected Extension's hasPermission result on existing subproperty");
+  });
+
+  await testExtension({
+    manifest: {
+      a_manifest_property: {
+        nested: {},
+      },
+    },
+    background() {
+      // Test hasPermission method implemented in ExtensionChild.jsm.
+      browser.test.assertTrue("testManifestPermission" in browser, "The API namespace is defined as expected");
+      browser.test.assertEq(
+        "function", browser.testManifestPermission && typeof browser.testManifestPermission.testMethod,
+        "The property with nested manifest property permission should be available ");
+      browser.test.notifyPass("test-extension-manifest-with-nested-prop");
+    },
+  }, async (extension) => {
+    await extension.awaitFinish("test-extension-manifest-with-nested-prop");
+
+    // Test hasPermission method implemented in Extension.jsm.
+    equal(extension.extension.hasPermission("manifest:a_manifest_property"), true,
+          "Got the expected Extension's hasPermission result on existing property");
+    equal(extension.extension.hasPermission("manifest:a_manifest_property.nested"), true,
+          "Got the expected Extension's hasPermission result on existing subproperty");
+    equal(extension.extension.hasPermission("manifest:a_manifest_property.unexisting"), false,
+          "Got the expected Extension's hasPermission result on non existing subproperty");
+  });
+});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_userScripts.js
@@ -594,17 +594,17 @@ add_task(async function test_userScripts
 
       browser.test.sendMessage("content-script:done");
     }
 
     let extension = ExtensionTestUtils.loadExtension({
       background,
       manifest: {
         permissions: ["http://*/*/file_sample.html"],
-        user_scripts: {},
+        user_scripts: {api_script: ""},
         content_scripts: [
           {
             matches:  ["http://*/*/file_sample.html"],
             js: ["content_script.js"],
             run_at: "document_start",
           },
         ],
       },
@@ -625,16 +625,50 @@ add_task(async function test_userScripts
     await extension.unload();
     await contentPage.close();
   }
 
   await runWithPrefs([["extensions.webextensions.userScripts.enabled", false]],
                      run_userScript_on_pref_disabled_test);
 });
 
+// This test verify that userScripts.setScriptAPIs is not available without
+// a "user_scripts.api_script" property in the manifest.
+add_task(async function test_user_script_api_script_required() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      content_scripts: [
+        {
+          matches: ["http://localhost/*/file_sample.html"],
+          js: ["content_script.js"],
+          run_at: "document_start",
+        },
+      ],
+      user_scripts: {},
+    },
+    files: {
+      "content_script.js": function() {
+        browser.test.assertEq(undefined, browser.userScripts && browser.userScripts.setScriptAPIs,
+                              "Got an undefined setScriptAPIs as expected");
+        browser.test.sendMessage("no-setScriptAPIs:done");
+      },
+    },
+  });
+
+  await extension.startup();
+
+  let url = `${BASE_URL}/file_sample.html`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(url);
+
+  await extension.awaitMessage("no-setScriptAPIs:done");
+
+  await extension.unload();
+  await contentPage.close();
+});
+
 add_task(async function test_scriptMetaData() {
   function getTestCases(isUserScriptsRegister) {
     return [
       // When scriptMetadata is not set (or undefined), it is treated as if it were null.
       // In the API script, the metadata is then expected to be null.
       isUserScriptsRegister ? undefined : null,
 
       // Falsey
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -38,16 +38,17 @@ tags = webextensions in-process-webexten
 [test_ext_manifest_minimum_chrome_version.js]
 [test_ext_manifest_minimum_opera_version.js]
 [test_ext_manifest_themes.js]
 [test_ext_schemas.js]
 [test_ext_schemas_roots.js]
 [test_ext_schemas_async.js]
 [test_ext_schemas_allowed_contexts.js]
 [test_ext_schemas_interactive.js]
+[test_ext_schemas_manifest_permissions.js]
 [test_ext_schemas_privileged.js]
 [test_ext_schemas_revoke.js]
 [test_ext_unknown_permissions.js]
 [test_locale_converter.js]
 [test_locale_data.js]
 [test_ext_ipcBlob.js]
 
 [test_ext_runtime_sendMessage_args.js]