Bug 1622917 - Use schema to determine whether to prompt about a permission r=mixedpuppy,snorp
authorAgi Sferro <agi@sferro.dev>
Mon, 13 Apr 2020 22:28:30 +0000
changeset 523724 1b598c787581f789ed1232903d46918a29897dfb
parent 523723 8049568017b73a3143572004fb750fc9cbab7617
child 523725 2c1a2c08592a2a62a1d94815b1e872ea83b0104d
push id37309
push useropoprus@mozilla.com
push dateTue, 14 Apr 2020 04:09:24 +0000
treeherdermozilla-central@dc5251d30a38 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy, snorp
bugs1622917
milestone77.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 1622917 - Use schema to determine whether to prompt about a permission r=mixedpuppy,snorp This allows other front-ends to know which permissions they should prompt for, like GeckoView. Differential Revision: https://phabricator.services.mozilla.com/D70102
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
toolkit/components/extensions/Extension.jsm
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
@@ -1812,17 +1812,18 @@ public class WebExtension {
             signedState = SignedStateFlags.UNKNOWN;
             disabledFlags = 0;
             enabled = true;
             baseUrl = null;
             allowedInPrivateBrowsing = false;
         }
 
         /* package */ MetaData(final GeckoBundle bundle) {
-            permissions = bundle.getStringArray("permissions");
+            // We only expose permissions that the embedder should prompt for
+            permissions = bundle.getStringArray("promptPermissions");
             origins = bundle.getStringArray("origins");
             description = bundle.getString("description");
             version = bundle.getString("version");
             creatorName = bundle.getString("creatorName");
             creatorUrl = bundle.getString("creatorURL");
             homepageUrl = bundle.getString("homepageURL");
             name = bundle.getString("name");
             optionsPageUrl = bundle.getString("optionsPageURL");
--- a/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewWebExtension.jsm
@@ -276,23 +276,26 @@ function exportExtension(aAddon, aPermis
   }
   let baseURL = "";
   let privateBrowsingAllowed = false;
   const policy = WebExtensionPolicy.getByID(id);
   if (policy) {
     baseURL = policy.getURL();
     privateBrowsingAllowed = policy.privateBrowsingAllowed;
   }
+  const promptPermissions = aPermissions
+    ? aPermissions.permissions.filter(Extension.shouldPromptFor)
+    : [];
   return {
     webExtensionId: id,
     locationURI: aSourceURI != null ? aSourceURI.spec : "",
     isBuiltIn: isBuiltin,
     metaData: {
-      permissions: aPermissions ? aPermissions.permissions : [],
       origins: aPermissions ? aPermissions.origins : [],
+      promptPermissions,
       description,
       enabled: isActive,
       disabledFlags,
       version,
       creatorName,
       creatorURL,
       homepageURL,
       name,
@@ -488,29 +491,35 @@ async function updatePromptHandler(aInfo
     // Updating from a legacy add-on, let it proceed
     return;
   }
 
   const newPerms = aInfo.addon.userPermissions;
 
   const difference = Extension.comparePermissions(oldPerms, newPerms);
 
+  // We only care about permissions that we can prompt the user for
+  const newPermissions = difference.permissions.filter(
+    Extension.shouldPromptFor
+  );
+  const { origins: newOrigins } = difference;
+
   // If there are no new permissions, just proceed
-  if (!difference.origins.length && !difference.permissions.length) {
+  if (!newOrigins.length && !newPermissions.length) {
     return;
   }
 
   const currentlyInstalled = exportExtension(aInfo.existingAddon, oldPerms);
   const updatedExtension = exportExtension(aInfo.addon, newPerms);
   const response = await EventDispatcher.instance.sendRequestForResult({
     type: "GeckoView:WebExtension:UpdatePrompt",
     currentlyInstalled,
     updatedExtension,
-    newPermissions: difference.permissions,
-    newOrigins: difference.origins,
+    newPermissions,
+    newOrigins,
   });
 
   if (!response.allow) {
     throw new Error("Extension update rejected.");
   }
 }
 
 var GeckoViewWebExtension = {
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -131,16 +131,25 @@ const { EventEmitter } = ExtensionCommon
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionCommon.getConsole);
 
 XPCOMUtils.defineLazyGetter(
   this,
   "LocaleData",
   () => ExtensionCommon.LocaleData
 );
 
+XPCOMUtils.defineLazyGetter(this, "NO_PROMPT_PERMISSIONS", () => {
+  return new Set(
+    Schemas.getPermissionNames([
+      "PermissionNoPrompt",
+      "OptionalPermissionNoPrompt",
+    ])
+  );
+});
+
 const { sharedData } = Services.ppmm;
 
 const PRIVATE_ALLOWED_PERMISSION = "internal:privateBrowsingAllowed";
 
 // The userContextID reserved for the extension storage (its purpose is ensuring that the IndexedDB
 // storage used by the browser.storage.local API is not directly accessible from the extension code,
 // it is defined and reserved as "userContextIdInternal.webextStorageLocal" in ContextualIdentityService.jsm).
 const WEBEXT_STORAGE_USER_CONTEXT_ID = -1 >>> 0;
@@ -671,16 +680,21 @@ class ExtensionData {
 
     const EXP_PATTERN = /^experiments\.\w+/;
     result.permissions = [...this.permissions].filter(
       p => !result.origins.includes(p) && !EXP_PATTERN.test(p)
     );
     return result;
   }
 
+  // Returns whether the front end should prompt for this permission
+  static shouldPromptFor(permission) {
+    return !NO_PROMPT_PERMISSIONS.has(permission);
+  }
+
   // Compute the difference between two sets of permissions, suitable
   // for presenting to the user.
   static comparePermissions(oldPermissions, newPermissions) {
     let oldMatcher = new MatchPatternSet(oldPermissions.origins, {
       restrictSchemes: false,
     });
     return {
       // formatPermissionStrings ignores any scheme, so only look at the domain.
@@ -1526,22 +1540,24 @@ class ExtensionData {
     // The permissions are sorted alphabetically by the permission
     // string to match AMO.
     let permissionsCopy = perms.permissions.slice(0);
     for (let permission of permissionsCopy.sort()) {
       // Handled above
       if (permission == "nativeMessaging") {
         continue;
       }
-      try {
-        result.msgs.push(bundle.GetStringFromName(permissionKey(permission)));
-      } catch (err) {
-        // We deliberately do not include all permissions in the prompt.
-        // So if we don't find one then just skip it.
+      if (!ExtensionData.shouldPromptFor(permission)) {
+        continue;
       }
+      // This will throw if the permission key is not present in the properties
+      // file. Any permission for which we prompt for needs an entry in
+      // browser.properties with the key
+      // |webextPerms.description.<permission name>|.
+      result.msgs.push(bundle.GetStringFromName(permissionKey(permission)));
     }
 
     const haveAccessKeys = AppConstants.platform !== "android";
 
     result.header = bundle.formatStringFromName("webextPerms.header", ["<>"]);
     result.text = info.unsigned
       ? bundle.GetStringFromName("webextPerms.unsignedWarning")
       : "";