tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ext-chrome-settings-overrides.js (20692B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 var { ExtensionPreferencesManager } = ChromeUtils.importESModule(
      8  "resource://gre/modules/ExtensionPreferencesManager.sys.mjs"
      9 );
     10 var { ExtensionParent } = ChromeUtils.importESModule(
     11  "resource://gre/modules/ExtensionParent.sys.mjs"
     12 );
     13 
     14 ChromeUtils.defineESModuleGetters(this, {
     15  ExtensionControlledPopup:
     16    "resource:///modules/ExtensionControlledPopup.sys.mjs",
     17  ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
     18  ExtensionSettingsStore:
     19    "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
     20  HomePage: "resource:///modules/HomePage.sys.mjs",
     21 });
     22 
     23 const DEFAULT_SEARCH_STORE_TYPE = "default_search";
     24 const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";
     25 
     26 const HOMEPAGE_PREF = "browser.startup.homepage";
     27 const HOMEPAGE_PRIVATE_ALLOWED =
     28  "browser.startup.homepage_override.privateAllowed";
     29 const HOMEPAGE_EXTENSION_CONTROLLED =
     30  "browser.startup.homepage_override.extensionControlled";
     31 const HOMEPAGE_CONFIRMED_TYPE = "homepageNotification";
     32 const HOMEPAGE_SETTING_TYPE = "prefs";
     33 const HOMEPAGE_SETTING_NAME = "homepage_override";
     34 
     35 ChromeUtils.defineLazyGetter(this, "homepagePopup", () => {
     36  return new ExtensionControlledPopup({
     37    confirmedType: HOMEPAGE_CONFIRMED_TYPE,
     38    observerTopic: "browser-open-homepage-start",
     39    popupnotificationId: "extension-homepage-notification",
     40    settingType: HOMEPAGE_SETTING_TYPE,
     41    settingKey: HOMEPAGE_SETTING_NAME,
     42    descriptionId: "extension-homepage-notification-description",
     43    descriptionMessageId: "homepageControlled.message",
     44    learnMoreLink: "extension-home",
     45    preferencesLocation: "home-homeOverride",
     46    preferencesEntrypoint: "addon-manage-home-override",
     47    async beforeDisableAddon(popup, win) {
     48      // Disabling an add-on should remove the tabs that it has open, but we want
     49      // to open the new homepage in this tab (which might get closed).
     50      //   1. Replace the tab's URL with about:blank, wait for it to change
     51      //   2. Now that this tab isn't associated with the add-on, disable the add-on
     52      //   3. Trigger the browser's homepage method
     53      let gBrowser = win.gBrowser;
     54      let tab = gBrowser.selectedTab;
     55      await replaceUrlInTab(gBrowser, tab, Services.io.newURI("about:blank"));
     56      Services.prefs.addObserver(HOMEPAGE_PREF, async function prefObserver() {
     57        Services.prefs.removeObserver(HOMEPAGE_PREF, prefObserver);
     58        let loaded = waitForTabLoaded(tab);
     59        win.BrowserCommands.home();
     60        await loaded;
     61        // Manually trigger an event in case this is controlled again.
     62        popup.open();
     63      });
     64    },
     65  });
     66 });
     67 
     68 // When the browser starts up it will trigger the observer topic we're expecting
     69 // but that happens before our observer has been registered. To handle the
     70 // startup case we need to check if the preferences are set to load the homepage
     71 // and check if the homepage is active, then show the doorhanger in that case.
     72 async function handleInitialHomepagePopup(extensionId, homepageUrl) {
     73  // browser.startup.page == 1 is show homepage.
     74  if (
     75    Services.prefs.getIntPref("browser.startup.page") == 1 &&
     76    windowTracker.topWindow
     77  ) {
     78    let { gBrowser } = windowTracker.topWindow;
     79    let tab = gBrowser.selectedTab;
     80    let currentUrl = gBrowser.currentURI.spec;
     81    // When the first window is still loading the URL might be about:blank.
     82    // Wait for that the actual page to load before checking the URL, unless
     83    // the homepage is set to about:blank.
     84    if (currentUrl != homepageUrl && currentUrl == "about:blank") {
     85      await waitForTabLoaded(tab);
     86      currentUrl = gBrowser.currentURI.spec;
     87    }
     88    // Once the page has loaded, if necessary and the active tab hasn't changed,
     89    // then show the popup now.
     90    if (currentUrl == homepageUrl && gBrowser.selectedTab == tab) {
     91      homepagePopup.open();
     92      return;
     93    }
     94  }
     95  homepagePopup.addObserver(extensionId);
     96 }
     97 
     98 /**
     99 * Handles the homepage url setting for an extension.
    100 *
    101 * @param {object} extension
    102 *   The extension setting the hompage url.
    103 * @param {string} homepageUrl
    104 *   The homepage url to set.
    105 */
    106 async function handleHomepageUrl(extension, homepageUrl) {
    107  // For new installs and enabling a disabled addon, we will show
    108  // the prompt.  We clear the confirmation in onDisabled and
    109  // onUninstalled, so in either ADDON_INSTALL or ADDON_ENABLE it
    110  // is already cleared, resulting in the prompt being shown if
    111  // necessary the next time the homepage is shown.
    112 
    113  // For localizing the homepageUrl, or otherwise updating the value
    114  // we need to always set the setting here.
    115  let inControl = await ExtensionPreferencesManager.setSetting(
    116    extension.id,
    117    "homepage_override",
    118    homepageUrl
    119  );
    120 
    121  if (inControl) {
    122    Services.prefs.setBoolPref(
    123      HOMEPAGE_PRIVATE_ALLOWED,
    124      extension.privateBrowsingAllowed
    125    );
    126    // Also set this now as an upgraded browser will need this.
    127    Services.prefs.setBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, true);
    128    if (extension.startupReason == "APP_STARTUP") {
    129      handleInitialHomepagePopup(extension.id, homepageUrl);
    130    } else {
    131      homepagePopup.addObserver(extension.id);
    132    }
    133  }
    134 
    135  // We need to monitor permission change and update the preferences.
    136  // eslint-disable-next-line mozilla/balanced-listeners
    137  extension.on("add-permissions", async (ignoreEvent, permissions) => {
    138    if (permissions.permissions.includes("internal:privateBrowsingAllowed")) {
    139      let item =
    140        await ExtensionPreferencesManager.getSetting("homepage_override");
    141      if (item && item.id == extension.id) {
    142        Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, true);
    143      }
    144    }
    145  });
    146  // eslint-disable-next-line mozilla/balanced-listeners
    147  extension.on("remove-permissions", async (ignoreEvent, permissions) => {
    148    if (permissions.permissions.includes("internal:privateBrowsingAllowed")) {
    149      let item =
    150        await ExtensionPreferencesManager.getSetting("homepage_override");
    151      if (item && item.id == extension.id) {
    152        Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, false);
    153      }
    154    }
    155  });
    156 }
    157 
    158 // When an extension starts up, a search engine may asynchronously be
    159 // registered, without blocking the startup. When an extension is
    160 // uninstalled, we need to wait for this registration to finish
    161 // before running the uninstallation handler.
    162 // Map[extension id -> Promise]
    163 var pendingSearchSetupTasks = new Map();
    164 
    165 this.chrome_settings_overrides = class extends ExtensionAPI {
    166  static async processDefaultSearchSetting(action, id) {
    167    await ExtensionSettingsStore.initialize();
    168    let item = ExtensionSettingsStore.getSetting(
    169      DEFAULT_SEARCH_STORE_TYPE,
    170      DEFAULT_SEARCH_SETTING_NAME,
    171      id
    172    );
    173    if (!item) {
    174      return;
    175    }
    176    let control = await ExtensionSettingsStore.getLevelOfControl(
    177      id,
    178      DEFAULT_SEARCH_STORE_TYPE,
    179      DEFAULT_SEARCH_SETTING_NAME
    180    );
    181    item = ExtensionSettingsStore[action](
    182      id,
    183      DEFAULT_SEARCH_STORE_TYPE,
    184      DEFAULT_SEARCH_SETTING_NAME
    185    );
    186    if (item && control == "controlled_by_this_extension") {
    187      try {
    188        let engine = Services.search.getEngineByName(
    189          item.value || item.initialValue
    190        );
    191        if (engine) {
    192          await Services.search.setDefault(
    193            engine,
    194            action == "enable"
    195              ? Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL
    196              : Ci.nsISearchService.CHANGE_REASON_ADDON_UNINSTALL
    197          );
    198        }
    199      } catch (e) {
    200        Cu.reportError(e);
    201      }
    202    }
    203  }
    204 
    205  static async removeEngine(id) {
    206    try {
    207      await Services.search.removeWebExtensionEngine(id);
    208    } catch (e) {
    209      Cu.reportError(e);
    210    }
    211  }
    212 
    213  static removeSearchSettings(id) {
    214    return Promise.all([
    215      this.processDefaultSearchSetting("removeSetting", id),
    216      this.removeEngine(id),
    217    ]);
    218  }
    219 
    220  static async onUninstall(id) {
    221    let searchStartupPromise = pendingSearchSetupTasks.get(id);
    222    if (searchStartupPromise) {
    223      await searchStartupPromise.catch(Cu.reportError);
    224    }
    225    // Note: We do not have to manage the homepage setting here
    226    // as it is managed by the ExtensionPreferencesManager.
    227    return Promise.all([
    228      this.removeSearchSettings(id),
    229      homepagePopup.clearConfirmation(id),
    230    ]);
    231  }
    232 
    233  static async onUpdate(id, manifest) {
    234    if (!manifest?.chrome_settings_overrides?.homepage) {
    235      // New or changed values are handled during onManifest.
    236      ExtensionPreferencesManager.removeSetting(id, "homepage_override");
    237    }
    238 
    239    let search_provider = manifest?.chrome_settings_overrides?.search_provider;
    240 
    241    if (!search_provider) {
    242      // Remove setting and engine from search if necessary.
    243      this.removeSearchSettings(id);
    244    } else if (!search_provider.is_default) {
    245      // Remove the setting, but keep the engine in search.
    246      chrome_settings_overrides.processDefaultSearchSetting(
    247        "removeSetting",
    248        id
    249      );
    250    }
    251  }
    252 
    253  static async onDisable(id) {
    254    homepagePopup.clearConfirmation(id);
    255 
    256    await chrome_settings_overrides.processDefaultSearchSetting("disable", id);
    257    await chrome_settings_overrides.removeEngine(id);
    258  }
    259 
    260  async onManifestEntry() {
    261    let { extension } = this;
    262    let { manifest } = extension;
    263    let homepageUrl = manifest.chrome_settings_overrides.homepage;
    264 
    265    // If this is a page we ignore, just skip the homepage setting completely.
    266    if (homepageUrl) {
    267      const ignoreHomePageUrl = await HomePage.shouldIgnore(homepageUrl);
    268 
    269      if (ignoreHomePageUrl) {
    270        Glean.homepage.preferenceIgnore.record({
    271          value: "set_blocked_extension",
    272          webExtensionId: extension.id,
    273        });
    274      } else {
    275        await handleHomepageUrl(extension, homepageUrl);
    276      }
    277    }
    278    if (manifest.chrome_settings_overrides.search_provider) {
    279      // Registering a search engine can potentially take a long while,
    280      // or not complete at all (when Services.search.promiseInitialized is
    281      // never resolved), so we are deliberately not awaiting the returned
    282      // promise here.
    283      let searchStartupPromise =
    284        this.processSearchProviderManifestEntry().finally(() => {
    285          if (
    286            pendingSearchSetupTasks.get(extension.id) === searchStartupPromise
    287          ) {
    288            pendingSearchSetupTasks.delete(extension.id);
    289            // This is primarily for tests so that we know when an extension
    290            // has finished initialising.
    291            ExtensionParent.apiManager.emit("searchEngineProcessed", extension);
    292          }
    293        });
    294 
    295      // Save the promise so we can await at onUninstall.
    296      pendingSearchSetupTasks.set(extension.id, searchStartupPromise);
    297    }
    298  }
    299 
    300  async ensureSetting(engineName, disable = false) {
    301    let { extension } = this;
    302    // Ensure the addon always has a setting
    303    await ExtensionSettingsStore.initialize();
    304    let item = ExtensionSettingsStore.getSetting(
    305      DEFAULT_SEARCH_STORE_TYPE,
    306      DEFAULT_SEARCH_SETTING_NAME,
    307      extension.id
    308    );
    309    if (!item) {
    310      let defaultEngine = await Services.search.getDefault();
    311      item = await ExtensionSettingsStore.addSetting(
    312        extension.id,
    313        DEFAULT_SEARCH_STORE_TYPE,
    314        DEFAULT_SEARCH_SETTING_NAME,
    315        engineName,
    316        () => defaultEngine.name
    317      );
    318      // If there was no setting, we're fixing old behavior in this api.
    319      // A lack of a setting would mean it was disabled before, disable it now.
    320      disable =
    321        disable ||
    322        ["ADDON_UPGRADE", "ADDON_DOWNGRADE", "ADDON_ENABLE"].includes(
    323          extension.startupReason
    324        );
    325    }
    326 
    327    // Ensure the item is disabled (either if exists and is not default or if it does not
    328    // exist yet).
    329    if (disable) {
    330      item = await ExtensionSettingsStore.disable(
    331        extension.id,
    332        DEFAULT_SEARCH_STORE_TYPE,
    333        DEFAULT_SEARCH_SETTING_NAME
    334      );
    335    }
    336    return item;
    337  }
    338 
    339  async promptDefaultSearch(engineName) {
    340    let { extension } = this;
    341    // Don't ask if it is already the current engine
    342    let engine = Services.search.getEngineByName(engineName);
    343    let defaultEngine = await Services.search.getDefault();
    344    if (defaultEngine.name == engine.name) {
    345      return;
    346    }
    347    // Ensures the setting exists and is disabled.  If the
    348    // user somehow bypasses the prompt, we do not want this
    349    // setting enabled for this extension.
    350    await this.ensureSetting(engineName, true);
    351 
    352    let subject = {
    353      wrappedJSObject: {
    354        // This is a hack because we don't have the browser of
    355        // the actual install. This means the popup might show
    356        // in a different window. Will be addressed in a followup bug.
    357        // As well, we still notify if no topWindow exists to support
    358        // testing from xpcshell.
    359        browser: windowTracker.topWindow?.gBrowser.selectedBrowser,
    360        id: extension.id,
    361        name: extension.name,
    362        icon: extension.getPreferredIcon(32),
    363        currentEngine: defaultEngine.name,
    364        newEngine: engineName,
    365        async respond(allow) {
    366          if (allow) {
    367            await chrome_settings_overrides.processDefaultSearchSetting(
    368              "enable",
    369              extension.id
    370            );
    371            await Services.search.setDefault(
    372              Services.search.getEngineByName(engineName),
    373              Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL
    374            );
    375          }
    376          // For testing
    377          Services.obs.notifyObservers(
    378            null,
    379            "webextension-defaultsearch-prompt-response"
    380          );
    381        },
    382      },
    383    };
    384    Services.obs.notifyObservers(subject, "webextension-defaultsearch-prompt");
    385  }
    386 
    387  async processSearchProviderManifestEntry() {
    388    let { extension } = this;
    389    let { manifest } = extension;
    390    let searchProvider = manifest.chrome_settings_overrides.search_provider;
    391 
    392    // If we're not being requested to be set as default, then all we need
    393    // to do is to add the engine to the service. The search service can cope
    394    // with receiving added engines before it is initialised, so we don't have
    395    // to wait for it.  Search Service will also prevent overriding a builtin
    396    // engine appropriately.
    397    if (!searchProvider.is_default) {
    398      await this.addSearchEngine();
    399      return;
    400    }
    401 
    402    await Services.search.promiseInitialized;
    403    if (!this.extension) {
    404      Cu.reportError(
    405        `Extension shut down before search provider was registered`
    406      );
    407      return;
    408    }
    409 
    410    let engineName = searchProvider.name.trim();
    411    let result = await Services.search.maybeSetAndOverrideDefault(extension);
    412    // This will only be set to true when the specified engine is a config
    413    // engine, or when it is an allowed add-on defined in the list stored in
    414    // SearchDefaultOverrideAllowlistHandler.
    415    if (result.canChangeToConfigEngine) {
    416      await this.setDefault(engineName, true);
    417    }
    418    if (!result.canInstallEngine) {
    419      // This extension is overriding a config search engine, so we don't
    420      // add its engine as well.
    421      return;
    422    }
    423    await this.addSearchEngine();
    424    if (extension.startupReason === "ADDON_INSTALL") {
    425      await this.promptDefaultSearch(engineName);
    426    } else {
    427      // Needs to be called every time to handle reenabling.
    428      await this.setDefault(engineName);
    429    }
    430  }
    431 
    432  async setDefault(engineName, skipEnablePrompt = false) {
    433    let { extension } = this;
    434 
    435    if (extension.startupReason === "ADDON_INSTALL") {
    436      // We should only get here if an extension is setting a config search
    437      // engine to default and we are ignoring the addons other engine settings.
    438      // In this case we do not show the prompt to the user.
    439      let item = await this.ensureSetting(engineName);
    440      await Services.search.setDefault(
    441        Services.search.getEngineByName(item.value),
    442        Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL
    443      );
    444    } else if (
    445      ["ADDON_UPGRADE", "ADDON_DOWNGRADE", "ADDON_ENABLE"].includes(
    446        extension.startupReason
    447      )
    448    ) {
    449      // We would be called for every extension being enabled, we should verify
    450      // that it has control and only then set it as default
    451      let control = await ExtensionSettingsStore.getLevelOfControl(
    452        extension.id,
    453        DEFAULT_SEARCH_STORE_TYPE,
    454        DEFAULT_SEARCH_SETTING_NAME
    455      );
    456 
    457      // Check for an inconsistency between the value returned by getLevelOfcontrol
    458      // and the current engine actually set.
    459      if (
    460        control === "controlled_by_this_extension" &&
    461        Services.search.defaultEngine.name !== engineName
    462      ) {
    463        // Check for and fix any inconsistency between the extensions settings storage
    464        // and the current engine actually set.  If settings claims the extension is default
    465        // but the search service claims otherwise, select what the search service claims
    466        // (See Bug 1767550).
    467        const allSettings = ExtensionSettingsStore.getAllSettings(
    468          DEFAULT_SEARCH_STORE_TYPE,
    469          DEFAULT_SEARCH_SETTING_NAME
    470        );
    471        for (const setting of allSettings) {
    472          if (setting.value !== Services.search.defaultEngine.name) {
    473            await ExtensionSettingsStore.disable(
    474              setting.id,
    475              DEFAULT_SEARCH_STORE_TYPE,
    476              DEFAULT_SEARCH_SETTING_NAME
    477            );
    478          }
    479        }
    480        control = await ExtensionSettingsStore.getLevelOfControl(
    481          extension.id,
    482          DEFAULT_SEARCH_STORE_TYPE,
    483          DEFAULT_SEARCH_SETTING_NAME
    484        );
    485      }
    486 
    487      if (control === "controlled_by_this_extension") {
    488        await Services.search.setDefault(
    489          Services.search.getEngineByName(engineName),
    490          Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL
    491        );
    492      } else if (control === "controllable_by_this_extension") {
    493        if (skipEnablePrompt) {
    494          // For overriding config engines, we don't prompt, so set
    495          // the default straight away.
    496          await chrome_settings_overrides.processDefaultSearchSetting(
    497            "enable",
    498            extension.id
    499          );
    500          await Services.search.setDefault(
    501            Services.search.getEngineByName(engineName),
    502            Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL
    503          );
    504        } else if (extension.startupReason == "ADDON_ENABLE") {
    505          // This extension has precedence, but is not in control.  Ask the user.
    506          await this.promptDefaultSearch(engineName);
    507        }
    508      }
    509    }
    510  }
    511 
    512  async addSearchEngine() {
    513    let { extension } = this;
    514    try {
    515      await Services.search.addEnginesFromExtension(extension);
    516    } catch (e) {
    517      Cu.reportError(e);
    518      return false;
    519    }
    520    return true;
    521  }
    522 };
    523 
    524 ExtensionPreferencesManager.addSetting("homepage_override", {
    525  prefNames: [
    526    HOMEPAGE_PREF,
    527    HOMEPAGE_EXTENSION_CONTROLLED,
    528    HOMEPAGE_PRIVATE_ALLOWED,
    529  ],
    530  // ExtensionPreferencesManager will call onPrefsChanged when control changes
    531  // and it updates the preferences. We are passed the item from
    532  // ExtensionSettingsStore that details what is in control. If there is an id
    533  // then control has changed to an extension, if there is no id then control
    534  // has been returned to the user.
    535  async onPrefsChanged(item) {
    536    if (item.id) {
    537      homepagePopup.addObserver(item.id);
    538 
    539      let policy = ExtensionParent.WebExtensionPolicy.getByID(item.id);
    540      let allowed = policy && policy.privateBrowsingAllowed;
    541      if (!policy) {
    542        // We'll generally hit this path during safe mode changes.
    543        let perms = await ExtensionPermissions.get(item.id);
    544        allowed = perms.permissions.includes("internal:privateBrowsingAllowed");
    545      }
    546      Services.prefs.setBoolPref(HOMEPAGE_PRIVATE_ALLOWED, allowed);
    547      Services.prefs.setBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, true);
    548    } else {
    549      homepagePopup.removeObserver();
    550 
    551      Services.prefs.clearUserPref(HOMEPAGE_PRIVATE_ALLOWED);
    552      Services.prefs.clearUserPref(HOMEPAGE_EXTENSION_CONTROLLED);
    553    }
    554  },
    555  setCallback(value) {
    556    // Setting the pref will result in onPrefsChanged being called, which
    557    // will then set HOMEPAGE_PRIVATE_ALLOWED.  We want to ensure that this
    558    // pref will be set/unset as apropriate.
    559    return {
    560      [HOMEPAGE_PREF]: value,
    561      [HOMEPAGE_EXTENSION_CONTROLLED]: !!value,
    562      [HOMEPAGE_PRIVATE_ALLOWED]: false,
    563    };
    564  },
    565 });