Bug 1389840: Part 2 - Store last optional permissions state in the startup cache. r=aswan
authorKris Maglione <maglione.k@gmail.com>
Sat, 12 Aug 2017 14:42:44 -0700 (2017-08-12)
changeset 375534 5b1ec4367b83568963639dc1a27840e4df4166aa
parent 375533 2a14d2f83f51bffb6c8378276b7f05a93a4e23de
child 375535 2a3c98277c1e45eda2e3b0fa0e3b317ff82a76dc
push id93951
push usermaglione.k@gmail.com
push dateFri, 18 Aug 2017 20:23:41 +0000 (2017-08-18)
treeherdermozilla-inbound@5b1ec4367b83 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan
bugs1389840
milestone57.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 1389840: Part 2 - Store last optional permissions state in the startup cache. r=aswan MozReview-Commit-ID: 95krDpu1JZr
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionParent.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -478,116 +478,129 @@ this.ExtensionData = class {
     // a *.domain.com to specific-host.domain.com that's actually a
     // drop in permissions but the simple test below will cause a prompt.
     return {
       origins: newPermissions.origins.filter(perm => !oldPermissions.origins.includes(perm)),
       permissions: newPermissions.permissions.filter(perm => !oldPermissions.permissions.includes(perm)),
     };
   }
 
-  parseManifest() {
-    return Promise.all([
+  async parseManifest() {
+    let [manifest] = await Promise.all([
       this.readJSON("manifest.json"),
       Management.lazyInit(),
-    ]).then(([manifest]) => {
-      this.manifest = manifest;
-      this.rawManifest = manifest;
+    ]);
+
+    this.manifest = manifest;
+    this.rawManifest = manifest;
+
+    if (manifest && manifest.default_locale) {
+      await this.initLocale();
+    }
+
+    let context = {
+      url: this.baseURI && this.baseURI.spec,
+
+      principal: this.principal,
 
-      if (manifest && manifest.default_locale) {
-        return this.initLocale();
+      logError: error => {
+        this.manifestWarning(error);
+      },
+
+      preprocessors: {},
+    };
+
+    if (this.manifest.theme) {
+      let invalidProps = validateThemeManifest(Object.getOwnPropertyNames(this.manifest));
+
+      if (invalidProps.length) {
+        let message = `Themes defined in the manifest may only contain static resources. ` +
+          `If you would like to use additional properties, please use the "theme" permission instead. ` +
+          `(the invalid properties found are: ${invalidProps})`;
+        this.manifestError(message);
       }
-    }).then(() => {
-      let context = {
-        url: this.baseURI && this.baseURI.spec,
-
-        principal: this.principal,
+    }
 
-        logError: error => {
-          this.manifestWarning(error);
-        },
+    if (this.localeData) {
+      context.preprocessors.localize = (value, context) => this.localize(value);
+    }
 
-        preprocessors: {},
-      };
+    let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
+    if (normalized.error) {
+      this.manifestError(normalized.error);
+      return null;
+    }
+
+    manifest = normalized.value;
 
-      if (this.manifest.theme) {
-        let invalidProps = validateThemeManifest(Object.getOwnPropertyNames(this.manifest));
+    let id;
+    try {
+      if (manifest.applications.gecko.id) {
+        id = manifest.applications.gecko.id;
+      }
+    } catch (e) {
+      // Errors are handled by the type checks above.
+    }
 
-        if (invalidProps.length) {
-          let message = `Themes defined in the manifest may only contain static resources. ` +
-            `If you would like to use additional properties, please use the "theme" permission instead. ` +
-            `(the invalid properties found are: ${invalidProps})`;
-          this.manifestError(message);
+    if (!this.id) {
+      this.id = id;
+    }
+
+    let apiNames = new Set();
+    let dependencies = new Set();
+    let originPermissions = new Set();
+    let permissions = new Set();
+
+    for (let perm of manifest.permissions) {
+      if (perm === "geckoProfiler") {
+        const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", "");
+        if (!acceptedExtensions.split(",").includes(id)) {
+          this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
+          continue;
         }
       }
 
-      if (this.localeData) {
-        context.preprocessors.localize = (value, context) => this.localize(value);
-      }
-
-      let normalized = Schemas.normalize(this.manifest, "manifest.WebExtensionManifest", context);
-      if (normalized.error) {
-        this.manifestError(normalized.error);
-        return null;
-      }
+      let type = classifyPermission(perm);
+      if (type.origin) {
+        let matcher = new MatchPattern(perm, {ignorePath: true});
 
-      let manifest = normalized.value;
-
-      let id;
-      try {
-        if (manifest.applications.gecko.id) {
-          id = manifest.applications.gecko.id;
-        }
-      } catch (e) {
-        // Errors are handled by the type checks above.
+        perm = matcher.pattern;
+        originPermissions.add(perm);
+      } else if (type.api) {
+        apiNames.add(type.api);
       }
 
-      let apiNames = new Set();
-      let dependencies = new Set();
-      let hostPermissions = new Set();
-      let permissions = new Set();
+      permissions.add(perm);
+    }
 
-      for (let perm of manifest.permissions) {
-        if (perm === "geckoProfiler") {
-          const acceptedExtensions = Services.prefs.getStringPref("extensions.geckoProfiler.acceptedExtensionIds", "");
-          if (!acceptedExtensions.split(",").includes(id)) {
-            this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler.");
-            continue;
-          }
-        }
+    if (this.id) {
+      // An extension always gets permission to its own url.
+      let matcher = new MatchPattern(this.getURL(), {ignorePath: true});
+      originPermissions.add(matcher.pattern);
 
-        let type = classifyPermission(perm);
-        if (type.origin) {
-          let matcher = new MatchPattern(perm, {ignorePath: true});
-
-          perm = matcher.pattern;
-          hostPermissions.add(perm);
-        } else if (type.api) {
-          apiNames.add(type.api);
-        }
-
+      // Apply optional permissions
+      let perms = await ExtensionPermissions.get(this);
+      for (let perm of perms.permissions) {
         permissions.add(perm);
       }
-
-      // An extension always gets permission to its own url.
-      if (this.id) {
-        let matcher = new MatchPattern(this.getURL(), {ignorePath: true});
-        hostPermissions.add(matcher.pattern);
+      for (let origin of perms.origins) {
+        originPermissions.add(origin);
       }
+    }
 
-      for (let api of apiNames) {
-        dependencies.add(`${api}@experiments.addons.mozilla.org`);
-      }
+    for (let api of apiNames) {
+      dependencies.add(`${api}@experiments.addons.mozilla.org`);
+    }
 
-      // Normalize all patterns to contain a single leading /
-      let webAccessibleResources = (manifest.web_accessible_resources || [])
-          .map(path => path.replace(/^\/*/, "/"));
+    // Normalize all patterns to contain a single leading /
+    let webAccessibleResources = (manifest.web_accessible_resources || [])
+        .map(path => path.replace(/^\/*/, "/"));
 
-      return {apiNames, dependencies, hostPermissions, id, manifest, permissions,
-              webAccessibleResources};
-    });
+    return {apiNames, dependencies, originPermissions, id, manifest, permissions,
+            webAccessibleResources};
   }
 
   // Reads the extension's |manifest.json| file, and stores its
   // parsed contents in |this.manifest|.
   async loadManifest() {
     let [manifestData] = await Promise.all([
       this.parseManifest(),
       Management.lazyInit(),
@@ -603,17 +616,17 @@ this.ExtensionData = class {
     }
 
     this.manifest = manifestData.manifest;
     this.apiNames = manifestData.apiNames;
     this.dependencies = manifestData.dependencies;
     this.permissions = manifestData.permissions;
 
     this.webAccessibleResources = manifestData.webAccessibleResources.map(res => new MatchGlob(res));
-    this.whiteListedHosts = new MatchPatternSet(manifestData.hostPermissions);
+    this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions);
 
     return this.manifest;
   }
 
   localizeMessage(...args) {
     return this.localeData.localizeMessage(...args);
   }
 
@@ -845,38 +858,42 @@ this.Extension = class extends Extension
     this.on("add-permissions", (ignoreEvent, permissions) => {
       for (let perm of permissions.permissions) {
         this.permissions.add(perm);
       }
 
       if (permissions.origins.length > 0) {
         let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
 
-        this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins],
+        this.whiteListedHosts = new MatchPatternSet(new Set([...patterns, ...permissions.origins]),
                                                     {ignorePath: true});
       }
 
       this.policy.permissions = Array.from(this.permissions);
       this.policy.allowedOrigins = this.whiteListedHosts;
+
+      this.cachePermissions();
     });
 
     this.on("remove-permissions", (ignoreEvent, permissions) => {
       for (let perm of permissions.permissions) {
         this.permissions.delete(perm);
       }
 
       let origins = permissions.origins.map(
         origin => new MatchPattern(origin, {ignorePath: true}).pattern);
 
       this.whiteListedHosts = new MatchPatternSet(
         this.whiteListedHosts.patterns
             .filter(host => !origins.includes(host.pattern)));
 
       this.policy.permissions = Array.from(this.permissions);
       this.policy.allowedOrigins = this.whiteListedHosts;
+
+      this.cachePermissions();
     });
     /* eslint-enable mozilla/balanced-listeners */
   }
 
   static getBootstrapScope(id, file) {
     return new BootstrapScope();
   }
 
@@ -967,19 +984,30 @@ this.Extension = class extends Extension
   readLocaleFile(locale) {
     return StartupCache.locales.get([this.id, this.version, locale],
                                     () => super.readLocaleFile(locale))
       .then(result => {
         this.localeData.messages.set(locale, result);
       });
   }
 
+  get manifestCacheKey() {
+    return [this.id, this.version, Services.locale.getAppLocaleAsLangTag()];
+  }
+
   parseManifest() {
-    return StartupCache.manifests.get([this.id, this.version, Services.locale.getAppLocaleAsLangTag()],
-                                      () => super.parseManifest());
+    return StartupCache.manifests.get(this.manifestCacheKey, () => super.parseManifest());
+  }
+
+  async cachePermissions() {
+    let manifestData = await this.parseManifest();
+
+    manifestData.originPermissions = this.whiteListedHosts.patterns.map(pat => pat.pattern);
+    manifestData.permissions = this.permissions;
+    return StartupCache.manifests.set(this.manifestCacheKey, manifestData);
   }
 
   async loadManifest() {
     let manifest = await super.loadManifest();
 
     if (this.errors.length) {
       return Promise.reject({errors: this.errors});
     }
@@ -1177,46 +1205,32 @@ this.Extension = class extends Extension
       // so during upgrades and add-on restarts, startup() gets called
       // before the last shutdown has completed, and this fails when
       // there's another active add-on with the same ID.
       this.policy.active = true;
     }
 
     TelemetryStopwatch.start("WEBEXT_EXTENSION_STARTUP_MS", this);
     try {
-      let [perms] = await Promise.all([
-        ExtensionPermissions.get(this),
-        this.loadManifest(),
-      ]);
+      await this.loadManifest();
 
       if (!this.hasShutdown) {
         await this.initLocale();
       }
 
       if (this.errors.length) {
         return Promise.reject({errors: this.errors});
       }
 
       if (this.hasShutdown) {
         return;
       }
 
       GlobalManager.init(this);
 
-      // Apply optional permissions
-      for (let perm of perms.permissions) {
-        this.permissions.add(perm);
-      }
-      if (perms.origins.length > 0) {
-        let patterns = this.whiteListedHosts.patterns.map(host => host.pattern);
-
-        this.whiteListedHosts = new MatchPatternSet([...patterns, ...perms.origins],
-                                                    {ignorePath: true});
-      }
-
       this.policy.active = false;
       this.policy = processScript.initExtension(this);
 
       this.updatePermissions(this.startupReason);
 
       // The "startup" Management event sent on the extension instance itself
       // is emitted just before the Management "startup" event,
       // and it is used to run code that needs to be executed before
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -1525,16 +1525,23 @@ class CacheStore {
       result = await createFunc(path);
       store.set(key, result);
       StartupCache.save();
     }
 
     return result;
   }
 
+  async set(path, value) {
+    let [store, key] = await this.getStore(path);
+
+    store.set(key, value);
+    StartupCache.save();
+  }
+
   async getAll() {
     let [store] = await this.getStore();
 
     return new Map(store);
   }
 
   async delete(path) {
     let [store, key] = await this.getStore(path);