Bug 1540253 - Define an isRecommended property for add-ons a=jcristau
authorRob Wu <rob@robwu.nl>
Mon, 17 Jun 2019 19:19:39 +0300
changeset 536988 51db6334c288ee26368ad74eabea4ab4f2e14104
parent 536987 f0aba1f30d061247e15506c1d792c1713f3b769f
child 536989 255b632c9f90cd1c8235eaf7346b5489c8c96afa
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjcristau
bugs1540253
milestone68.0
Bug 1540253 - Define an isRecommended property for add-ons a=jcristau Reviewers: rpl, mstriemer, aswan Reviewed By: aswan Subscribers: aswan Bug #: 1540253 Differential Revision: https://phabricator.services.mozilla.com/D34667
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/test/xpcshell/test_recommendations.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -110,17 +110,17 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
                           "skinnable", "sourceURI", "releaseNotesURI",
                           "softDisabled", "foreignInstall",
                           "strictCompatibility", "locales", "targetApplications",
                           "targetPlatforms", "signedState",
                           "seen", "dependencies", "incognito",
                           "userPermissions", "icons", "iconURL",
                           "blocklistState", "blocklistURL", "startupData",
                           "previewImage", "hidden", "installTelemetryInfo",
-                          "rootURI"];
+                          "recommendationState", "rootURI"];
 
 const LEGACY_TYPES = new Set([
   "extension",
 ]);
 
 const SIGNED_TYPES = new Set([
   "extension",
   "locale",
@@ -241,16 +241,17 @@ class AddonInternal {
     this.foreignInstall = false;
     this.seen = true;
     this.skinnable = false;
     this.startupData = null;
     this._hidden = false;
     this.installTelemetryInfo = null;
     this.rootURI = null;
     this._updateInstall = null;
+    this.recommendationState = null;
 
     this.inDatabase = false;
 
     /**
      * @property {Array<string>} dependencies
      *   An array of bootstrapped add-on IDs on which this add-on depends.
      *   The add-on will remain appDisabled if any of the dependent
      *   add-ons is not installed and enabled.
@@ -828,16 +829,28 @@ AddonWrapper = class {
     if (addon.previewImage) {
       let url = this.getResourceURI(addon.previewImage).spec;
       return [new AddonManagerPrivate.AddonScreenshot(url)];
     }
 
     return null;
   }
 
+  get isRecommended() {
+    let addon = addonFor(this);
+    let state = addon.recommendationState;
+    if (state &&
+        state.validNotBefore < addon.updateDate &&
+        state.validNotAfter > addon.updateDate &&
+        addon.isCorrectlySigned && !this.temporarilyInstalled) {
+      return state.states.includes("recommended");
+    }
+    return false;
+  }
+
   get applyBackgroundUpdates() {
     return addonFor(this).applyBackgroundUpdates;
   }
   set applyBackgroundUpdates(val) {
     let addon = addonFor(this);
     if (val != AddonManager.AUTOUPDATE_DEFAULT &&
         val != AddonManager.AUTOUPDATE_DISABLE &&
         val != AddonManager.AUTOUPDATE_ENABLE) {
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -528,16 +528,46 @@ async function loadManifestFromWebManife
   addon.targetPlatforms = [];
   // Themes are disabled by default, except when they're installed from a web page.
   addon.userDisabled = (extension.type === "theme");
   addon.softDisabled = addon.blocklistState == nsIBlocklistService.STATE_SOFTBLOCKED;
 
   return addon;
 }
 
+async function readRecommendationStates(aPackage, aAddonID) {
+  let recommendationData;
+  try {
+    recommendationData = JSON.parse(await aPackage.readString("mozilla-recommendation.json"));
+  } catch (e) {
+    if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+      logger.warn("Failed to parse recommendation", e);
+    }
+  }
+
+  if (recommendationData) {
+    let {addon_id, states, validity} = recommendationData;
+
+    if (addon_id === aAddonID && Array.isArray(states) && validity) {
+      let validNotAfter = Date.parse(validity.not_after);
+      let validNotBefore = Date.parse(validity.not_before);
+      if (validNotAfter && validNotBefore) {
+        return {
+          validNotAfter,
+          validNotBefore,
+          states,
+        };
+      }
+    }
+    logger.warn(`Invalid recommendation for ${aAddonID}: ${JSON.stringify(recommendationData)}`);
+  }
+
+  return null;
+}
+
 function defineSyncGUID(aAddon) {
   // Define .syncGUID as a lazy property which is also settable
   Object.defineProperty(aAddon, "syncGUID", {
     get: () => {
       aAddon.syncGUID = uuidGen.generateUUID().toString();
       return aAddon.syncGUID;
     },
     set: (val) => {
@@ -597,16 +627,20 @@ var loadManifest = async function(aPacka
         throw new Error(`Extension is signed with an invalid id (${addon.id})`);
       }
     }
     if (!addon.id && aLocation.isTemporary) {
       addon.id = generateTemporaryInstallID(aPackage.file);
     }
   }
 
+  if (addon.type === "extension") {
+    addon.recommendationState = await readRecommendationStates(aPackage, addon.id);
+  }
+
   addon.propagateDisabledState(aOldAddon);
   await addon.updateBlocklistState();
   addon.appDisabled = !XPIDatabase.isUsableAddon(addon);
 
   defineSyncGUID(addon);
 
   return addon;
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_recommendations.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {XPIInstall} =
+  ChromeUtils.import("resource://gre/modules/addons/XPIInstall.jsm");
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+const testStartTime = Date.now();
+const not_before = new Date(testStartTime - 3600000).toISOString();
+const not_after = new Date(testStartTime + 3600000).toISOString();
+const RECOMMENDATION_FILE_NAME = "mozilla-recommendation.json";
+
+function createFileWithRecommendations(id, recommendation) {
+  let files = {};
+  if (recommendation) {
+    files[RECOMMENDATION_FILE_NAME] = recommendation;
+  }
+  return AddonTestUtils.createTempWebExtensionFile({
+    manifest: {
+      applications: {gecko: {id}},
+    },
+    files,
+  });
+}
+
+async function installAddonWithRecommendations(id, recommendation) {
+  let xpi = createFileWithRecommendations(id, recommendation);
+  let install = await AddonTestUtils.promiseInstallFile(xpi);
+  return install.addon;
+}
+
+add_task(async function setup() {
+  await ExtensionTestUtils.startAddonManager();
+});
+
+add_task(async function text_no_file() {
+  const id = "no-recommendations-file@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, null);
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function text_malformed_file() {
+  const id = "no-recommendations-file@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, "This is not JSON");
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_valid_recommendation_file() {
+  const id = "recommended@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: ["recommended"],
+    validity: {not_before, not_after},
+  });
+
+  ok(addon.isRecommended, "The add-on is recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_unsigned() {
+  // Don't override the certificate, so that the test add-on is unsigned.
+  AddonTestUtils.useRealCertChecks = true;
+  // Allow unsigned add-on to be installed.
+  Services.prefs.setBoolPref("xpinstall.signatures.required", false);
+
+  const id = "unsigned@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: ["recommended"],
+    validity: {not_before, not_after},
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+  AddonTestUtils.useRealCertChecks = false;
+  Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+});
+
+add_task(async function test_temporary() {
+  const id = "temporary@test.web.extension";
+  let xpi = createFileWithRecommendations(id, {
+    addon_id: id,
+    states: ["recommended"],
+    validity: {not_before, not_after},
+  });
+  let addon = await XPIInstall.installTemporaryAddon(xpi);
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_theme() {
+  const id = "theme@test.web.extension";
+  let xpi = AddonTestUtils.createTempWebExtensionFile({
+    manifest: {
+      applications: {gecko: {id}},
+      theme: {},
+    },
+    files: {
+      [RECOMMENDATION_FILE_NAME]: {
+        addon_id: id,
+        states: ["recommended"],
+        validity: {not_before, not_after},
+      },
+    },
+  });
+  let {addon} = await AddonTestUtils.promiseInstallFile(xpi);
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_not_recommended() {
+  const id = "not-recommended@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: ["something"],
+    validity: {not_before, not_after},
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_id_missing() {
+  const id = "no-id@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    states: ["recommended"],
+    validity: {not_before, not_after},
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_expired() {
+  const id = "expired@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: ["recommended"],
+    validity: {not_before, not_after: not_before},
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_not_valid_yet() {
+  const id = "expired@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: ["recommended"],
+    validity: {not_before: not_after, not_after},
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_states_missing() {
+  const id = "states-missing@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    validity: {not_before, not_after},
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_validity_missing() {
+  const id = "validity-missing@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: ["recommended"],
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_not_before_missing() {
+  const id = "not-before-missing@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: ["recommended"],
+    validity: {not_after},
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_bad_states() {
+  const id = "bad-states@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: {recommended: true},
+    validity: {not_before, not_after},
+  });
+
+  ok(!addon.isRecommended, "The add-on is not recommended");
+
+  await addon.uninstall();
+});
+
+add_task(async function test_recommendation_persist_restart() {
+  const id = "persisted-recommendation@test.web.extension";
+  let addon = await installAddonWithRecommendations(id, {
+    addon_id: id,
+    states: ["recommended"],
+    validity: {not_before, not_after},
+  });
+
+  ok(addon.isRecommended, "The add-on is recommended");
+
+  await AddonTestUtils.promiseRestartManager();
+
+  addon = await AddonManager.getAddonByID(id);
+
+  ok(addon.isRecommended, "The add-on is still recommended");
+
+  await addon.uninstall();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -72,16 +72,17 @@ skip-if = os == "android"
 [test_plugins.js]
 [test_pref_properties.js]
 [test_provider_markSafe.js]
 [test_provider_shutdown.js]
 [test_provider_unsafe_access_shutdown.js]
 [test_provider_unsafe_access_startup.js]
 [test_proxies.js]
 skip-if = require_signing
+[test_recommendations.js]
 [test_registerchrome.js]
 [test_registry.js]
 skip-if = os != 'win'
 [test_reinstall_disabled_addon.js]
 [test_reload.js]
 # Bug 676992: test consistently hangs on Android
 # There's a problem removing a temp file without manually clearing the cache on Windows
 skip-if = os == "android" || os == "win"