toolkit/components/extensions/ExtensionPreferencesManager.jsm
author Dorel Luca <dluca@mozilla.com>
Wed, 25 Apr 2018 21:43:05 +0300
changeset 469081 1d99f96c0aec2f65e4d17dd3d11a46615f1d690a
parent 469077 b7a347f011cb8c1eb15e8357f2dae0f97eef9bd8
child 469227 b62ad926cf2a2d5759222f4e9b40c9e3bd3bdd31
permissions -rw-r--r--
Backed out changeset b7a347f011cb (bug 1455755) for browser chrome failure on browser/components/preferences/in-content/tests/browser_extension_controlled.js. CLOSED TREE

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

/**
 * @fileOverview
 * This module is used for managing preferences from WebExtension APIs.
 * It takes care of the precedence chain and decides whether a preference
 * needs to be updated when a change is requested by an API.
 *
 * It deals with preferences via settings objects, which are objects with
 * the following properties:
 *
 * prefNames:   An array of strings, each of which is a preference on
 *              which the setting depends.
 * setCallback: A function that returns an object containing properties and
 *              values that correspond to the prefs to be set.
 */

var EXPORTED_SYMBOLS = ["ExtensionPreferencesManager"];

const {Management} = ChromeUtils.import("resource://gre/modules/Extension.jsm", {});

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");

ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
                               "resource://gre/modules/ExtensionSettingsStore.jsm");
ChromeUtils.defineModuleGetter(this, "Preferences",
                               "resource://gre/modules/Preferences.jsm");

XPCOMUtils.defineLazyGetter(this, "defaultPreferences", function() {
  return new Preferences({defaultBranch: true});
});

/* eslint-disable mozilla/balanced-listeners */
Management.on("uninstall", (type, {id}) => {
  ExtensionPreferencesManager.removeAll(id);
});

Management.on("shutdown", (type, extension) => {
  if (extension.shutdownReason == "ADDON_DISABLE") {
    this.ExtensionPreferencesManager.disableAll(extension.id);
  }
});

Management.on("startup", async (type, extension) => {
  if (extension.startupReason == "ADDON_ENABLE") {
    this.ExtensionPreferencesManager.enableAll(extension.id);
  }
});
/* eslint-enable mozilla/balanced-listeners */

const STORE_TYPE = "prefs";

// Definitions of settings, each of which correspond to a different API.
let settingsMap = new Map();

/**
 * This function is passed into the ExtensionSettingsStore to determine the
 * initial value of the setting. It reads an array of preference names from
 * the this scope, which gets bound to a settings object.
 *
 * @returns {Object}
 *          An object with one property per preference, which holds the current
 *          value of that preference.
 */
function initialValueCallback() {
  let initialValue = {};
  for (let pref of this.prefNames) {
    initialValue[pref] = Preferences.get(pref);
  }
  return initialValue;
}

/**
 * Loops through a set of prefs, either setting or resetting them.
 *
 * @param {Object} setting
 *        An object that represents a setting, which will have a setCallback
 *        property. If a onPrefsChanged function is provided it will be called
 *        with item when the preferences change.
 * @param {Object} item
 *        An object that represents an item handed back from the setting store
 *        from which the new pref values can be calculated.
*/
function setPrefs(setting, item) {
  let prefs = item.initialValue || setting.setCallback(item.value);
  let changed = false;
  for (let pref in prefs) {
    if (prefs[pref] === undefined) {
      if (Preferences.isSet(pref)) {
        changed = true;
        Preferences.reset(pref);
      }
    } else if (Preferences.get(pref) != prefs[pref]) {
      Preferences.set(pref, prefs[pref]);
      changed = true;
    }
  }
  if (changed && typeof setting.onPrefsChanged == "function") {
    setting.onPrefsChanged(item);
  }
}

/**
 * Commits a change to a setting and conditionally sets preferences.
 *
 * If the change to the setting causes a different extension to gain
 * control of the pref (or removes all extensions with control over the pref)
 * then the prefs should be updated, otherwise they should not be.
 * In addition, if the current value of any of the prefs does not
 * match what we expect the value to be (which could be the result of a
 * user manually changing the pref value), then we do not change any
 * of the prefs.
 *
 * @param {string} id
 *        The id of the extension for which a setting is being modified.
 * @param {string} name
 *        The name of the setting being processed.
 * @param {string} action
 *        The action that is being performed. Will be one of disable, enable
 *        or removeSetting.

 * @returns {Promise}
 *          Resolves to true if preferences were set as a result and to false
 *          if preferences were not set.
*/
async function processSetting(id, name, action) {
  await ExtensionSettingsStore.initialize();
  let expectedItem = ExtensionSettingsStore.getSetting(STORE_TYPE, name);
  let item = ExtensionSettingsStore[action](id, STORE_TYPE, name);
  if (item) {
    let setting = settingsMap.get(name);
    let expectedPrefs = expectedItem.initialValue
      || setting.setCallback(expectedItem.value);
    if (Object.keys(expectedPrefs)
              .some(pref => (expectedPrefs[pref] &&
                             Preferences.get(pref) != expectedPrefs[pref]))) {
      return false;
    }
    setPrefs(setting, item);
    return true;
  }
  return false;
}

this.ExtensionPreferencesManager = {
  /**
   * Adds a setting to the settingsMap. This is how an API tells the
   * preferences manager what its setting object is. The preferences
   * manager needs to know this when settings need to be removed
   * automatically.
   *
   * @param {string} name The unique id of the setting.
   * @param {Object} setting
   *        A setting object that should have properties for
   *        prefNames, getCallback and setCallback.
   */
  addSetting(name, setting) {
    settingsMap.set(name, setting);
  },

  /**
   * Gets the default value for a preference.
   *
   * @param {string} prefName The name of the preference.
   *
   * @returns {string|number|boolean} The default value of the preference.
   */
  getDefaultValue(prefName) {
    return defaultPreferences.get(prefName);
  },

  /**
   * Indicates that an extension would like to change the value of a previously
   * defined setting.
   *
   * @param {string} id
   *        The id of the extension for which a setting is being set.
   * @param {string} name
   *        The unique id of the setting.
   * @param {any} value
   *        The value to be stored in the settings store for this
   *        group of preferences.
   *
   * @returns {Promise}
   *          Resolves to true if the preferences were changed and to false if
   *          the preferences were not changed.
   */
  async setSetting(id, name, value) {
    let setting = settingsMap.get(name);
    await ExtensionSettingsStore.initialize();
    let item = await ExtensionSettingsStore.addSetting(
      id, STORE_TYPE, name, value, initialValueCallback.bind(setting));
    if (item) {
      setPrefs(setting, item);
      return true;
    }
    return false;
  },

  /**
   * Indicates that this extension wants to temporarily cede control over the
   * given setting.
   *
   * @param {string} id
   *        The id of the extension for which a preference setting is being disabled.
   * @param {string} name
   *        The unique id of the setting.
   *
   * @returns {Promise}
   *          Resolves to true if the preferences were changed and to false if
   *          the preferences were not changed.
   */
  disableSetting(id, name) {
    return processSetting(id, name, "disable");
  },

  /**
   * Enable a setting that has been disabled.
   *
   * @param {string} id
   *        The id of the extension for which a setting is being enabled.
   * @param {string} name
   *        The unique id of the setting.
   *
   * @returns {Promise}
   *          Resolves to true if the preferences were changed and to false if
   *          the preferences were not changed.
   */
  enableSetting(id, name) {
    return processSetting(id, name, "enable");
  },

  /**
   * Indicates that this extension no longer wants to set the given setting.
   *
   * @param {string} id
   *        The id of the extension for which a preference setting is being removed.
   * @param {string} name
   *        The unique id of the setting.
   *
   * @returns {Promise}
   *          Resolves to true if the preferences were changed and to false if
   *          the preferences were not changed.
   */
  removeSetting(id, name) {
    return processSetting(id, name, "removeSetting");
  },

  /**
   * Disables all previously set settings for an extension. This can be called when
   * an extension is being disabled, for example.
   *
   * @param {string} id
   *        The id of the extension for which all settings are being unset.
   */
  async disableAll(id) {
    await ExtensionSettingsStore.initialize();
    let settings = ExtensionSettingsStore.getAllForExtension(id, STORE_TYPE);
    let disablePromises = [];
    for (let name of settings) {
      disablePromises.push(this.disableSetting(id, name));
    }
    await Promise.all(disablePromises);
  },

  /**
   * Enables all disabled settings for an extension. This can be called when
   * an extension has finished updating or is being re-enabled, for example.
   *
   * @param {string} id
   *        The id of the extension for which all settings are being enabled.
   */
  async enableAll(id) {
    await ExtensionSettingsStore.initialize();
    let settings = ExtensionSettingsStore.getAllForExtension(id, STORE_TYPE);
    let enablePromises = [];
    for (let name of settings) {
      enablePromises.push(this.enableSetting(id, name));
    }
    await Promise.all(enablePromises);
  },

  /**
   * Removes all previously set settings for an extension. This can be called when
   * an extension is being uninstalled, for example.
   *
   * @param {string} id
   *        The id of the extension for which all settings are being unset.
   */
  async removeAll(id) {
    await ExtensionSettingsStore.initialize();
    let settings = ExtensionSettingsStore.getAllForExtension(id, STORE_TYPE);
    let removePromises = [];
    for (let name of settings) {
      removePromises.push(this.removeSetting(id, name));
    }
    await Promise.all(removePromises);
  },

  /**
   * Return the currently active value for a setting.
   *
   * @param {string} name
   *        The unique id of the setting.
   *
   * @returns {Object} The current setting object.
   */
  async getSetting(name) {
    await ExtensionSettingsStore.initialize();
    return ExtensionSettingsStore.getSetting(STORE_TYPE, name);
  },

  /**
   * Return the levelOfControl for a setting / extension combo.
   * This queries the levelOfControl from the ExtensionSettingsStore and also
   * takes into account whether any of the setting's preferences are locked.
   *
   * @param {string} id
   *        The id of the extension for which levelOfControl is being requested.
   * @param {string} name
   *        The unique id of the setting.
   * @param {string} storeType
   *        The name of the store in ExtensionSettingsStore.
   *        Defaults to STORE_TYPE.
   *
   * @returns {Promise}
   *          Resolves to the level of control of the extension over the setting.
   */
  async getLevelOfControl(id, name, storeType = STORE_TYPE) {
    // This could be called for a setting that isn't defined to the PreferencesManager,
    // in which case we simply defer to the SettingsStore.
    if (storeType === STORE_TYPE) {
      let setting = settingsMap.get(name);
      if (!setting) {
        return "not_controllable";
      }
      for (let prefName of setting.prefNames) {
        if (Preferences.locked(prefName)) {
          return "not_controllable";
        }
      }
    }
    await ExtensionSettingsStore.initialize();
    return ExtensionSettingsStore.getLevelOfControl(id, storeType, name);
  },
};