tor-browser

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

addonutils.sys.mjs (13092B)


      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 import { Log } from "resource://gre/modules/Log.sys.mjs";
      6 
      7 import { Svc } from "resource://services-sync/util.sys.mjs";
      8 
      9 const lazy = {};
     10 
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     13  AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
     14 });
     15 
     16 function AddonUtilsInternal() {
     17  this._log = Log.repository.getLogger("Sync.AddonUtils");
     18  this._log.Level =
     19    Log.Level[Svc.PrefBranch.getStringPref("log.logger.addonutils", null)];
     20 }
     21 AddonUtilsInternal.prototype = {
     22  /**
     23   * Obtain an AddonInstall object from an AddonSearchResult instance.
     24   *
     25   * The returned promise will be an AddonInstall on success or null (failure or
     26   * addon not found)
     27   *
     28   * @param addon
     29   *        AddonSearchResult to obtain install from.
     30   */
     31  getInstallFromSearchResult(addon) {
     32    this._log.debug("Obtaining install for " + addon.id);
     33 
     34    // We should theoretically be able to obtain (and use) addon.install if
     35    // it is available. However, the addon.sourceURI rewriting won't be
     36    // reflected in the AddonInstall, so we can't use it. If we ever get rid
     37    // of sourceURI rewriting, we can avoid having to reconstruct the
     38    // AddonInstall.
     39    return lazy.AddonManager.getInstallForURL(addon.sourceURI.spec, {
     40      name: addon.name,
     41      icons: addon.iconURL,
     42      version: addon.version,
     43      telemetryInfo: { source: "sync" },
     44    });
     45  },
     46 
     47  /**
     48   * Installs an add-on from an AddonSearchResult instance.
     49   *
     50   * The options argument defines extra options to control the install.
     51   * Recognized keys in this map are:
     52   *
     53   *   syncGUID - Sync GUID to use for the new add-on.
     54   *   enabled - Boolean indicating whether the add-on should be enabled upon
     55   *             install.
     56   *
     57   * The result object has the following keys:
     58   *
     59   *   id      ID of add-on that was installed.
     60   *   install AddonInstall that was installed.
     61   *   addon   Addon that was installed.
     62   *
     63   * @param addon
     64   *        AddonSearchResult to install add-on from.
     65   * @param options
     66   *        Object with additional metadata describing how to install add-on.
     67   */
     68  async installAddonFromSearchResult(addon, options) {
     69    this._log.info("Trying to install add-on from search result: " + addon.id);
     70 
     71    const install = await this.getInstallFromSearchResult(addon);
     72    if (!install) {
     73      throw new Error("AddonInstall not available: " + addon.id);
     74    }
     75 
     76    try {
     77      this._log.info("Installing " + addon.id);
     78      let log = this._log;
     79 
     80      return new Promise((res, rej) => {
     81        let listener = {
     82          onInstallStarted: function onInstallStarted(install) {
     83            if (!options) {
     84              return;
     85            }
     86 
     87            if (options.syncGUID) {
     88              log.info(
     89                "Setting syncGUID of " + install.name + ": " + options.syncGUID
     90              );
     91              install.addon.syncGUID = options.syncGUID;
     92            }
     93 
     94            // We only need to change userDisabled if it is disabled because
     95            // enabled is the default.
     96            if ("enabled" in options && !options.enabled) {
     97              log.info(
     98                "Marking add-on as disabled for install: " + install.name
     99              );
    100              install.addon.disable();
    101            }
    102          },
    103          onInstallEnded(install, addon) {
    104            // Themes-addons are not automatically enabled when installed, but the active
    105            // theme is synced by prefs, so we use a transient pref to enable it
    106            // after install
    107            let hasIncomingActiveThemeId = Services.prefs.getStringPref(
    108              "extensions.pendingActiveThemeID",
    109              ""
    110            );
    111            if (
    112              hasIncomingActiveThemeId &&
    113              addon.id === hasIncomingActiveThemeId
    114            ) {
    115              try {
    116                addon.enable();
    117              } catch (e) {
    118                this._log.error("Failed to enable the incoming theme", e);
    119              } finally {
    120                // If something went wrong with enabling the theme, we don't have a good
    121                // way to retry -- so we'll clear it rather than keeping the pref around
    122                Services.prefs.clearUserPref("extensions.pendingActiveThemeID");
    123              }
    124            }
    125            install.removeListener(listener);
    126 
    127            res({ id: addon.id, install, addon });
    128          },
    129          onInstallFailed(install) {
    130            install.removeListener(listener);
    131 
    132            rej(new Error("Install failed: " + install.error));
    133          },
    134          onDownloadFailed(install) {
    135            install.removeListener(listener);
    136 
    137            rej(new Error("Download failed: " + install.error));
    138          },
    139        };
    140        install.addListener(listener);
    141        install.install();
    142      });
    143    } catch (ex) {
    144      this._log.error("Error installing add-on", ex);
    145      throw ex;
    146    }
    147  },
    148 
    149  /**
    150   * Uninstalls the addon instance.
    151   *
    152   * @param addon
    153   *        Addon instance to uninstall.
    154   */
    155  async uninstallAddon(addon) {
    156    return new Promise(res => {
    157      let listener = {
    158        onUninstalling(uninstalling, needsRestart) {
    159          if (addon.id != uninstalling.id) {
    160            return;
    161          }
    162 
    163          // We assume restartless add-ons will send the onUninstalled event
    164          // soon.
    165          if (!needsRestart) {
    166            return;
    167          }
    168 
    169          // For non-restartless add-ons, we issue the callback on uninstalling
    170          // because we will likely never see the uninstalled event.
    171          lazy.AddonManager.removeAddonListener(listener);
    172          res(addon);
    173        },
    174        onUninstalled(uninstalled) {
    175          if (addon.id != uninstalled.id) {
    176            return;
    177          }
    178 
    179          lazy.AddonManager.removeAddonListener(listener);
    180          res(addon);
    181        },
    182      };
    183      lazy.AddonManager.addAddonListener(listener);
    184      addon.uninstall();
    185    });
    186  },
    187 
    188  /**
    189   * Installs multiple add-ons specified by metadata.
    190   *
    191   * The first argument is an array of objects. Each object must have the
    192   * following keys:
    193   *
    194   *   id - public ID of the add-on to install.
    195   *   syncGUID - syncGUID for new add-on.
    196   *   enabled - boolean indicating whether the add-on should be enabled.
    197   *   requireSecureURI - Boolean indicating whether to require a secure
    198   *     URI when installing from a remote location. This defaults to
    199   *     true.
    200   *
    201   * The callback will be called when activity on all add-ons is complete. The
    202   * callback receives 2 arguments, error and result.
    203   *
    204   * If error is truthy, it contains a string describing the overall error.
    205   *
    206   * The 2nd argument to the callback is always an object with details on the
    207   * overall execution state. It contains the following keys:
    208   *
    209   *   installedIDs  Array of add-on IDs that were installed.
    210   *   installs      Array of AddonInstall instances that were installed.
    211   *   addons        Array of Addon instances that were installed.
    212   *   errors        Array of errors encountered. Only has elements if error is
    213   *                 truthy.
    214   *
    215   * @param installs
    216   *        Array of objects describing add-ons to install.
    217   */
    218  async installAddons(installs) {
    219    let ids = [];
    220    for (let addon of installs) {
    221      ids.push(addon.id);
    222    }
    223 
    224    let addons = await lazy.AddonRepository.getAddonsByIDs(ids);
    225    this._log.info(
    226      `Found ${addons.length} / ${ids.length}` +
    227        " add-ons during repository search."
    228    );
    229 
    230    let ourResult = {
    231      installedIDs: [],
    232      installs: [],
    233      addons: [],
    234      skipped: [],
    235      errors: [],
    236    };
    237 
    238    let toInstall = [];
    239 
    240    // Rewrite the "src" query string parameter of the source URI to note
    241    // that the add-on was installed by Sync and not something else so
    242    // server-side metrics aren't skewed (bug 708134). The server should
    243    // ideally send proper URLs, but this solution was deemed too
    244    // complicated at the time the functionality was implemented.
    245    for (let addon of addons) {
    246      // Find the specified options for this addon.
    247      let options;
    248      for (let install of installs) {
    249        if (install.id == addon.id) {
    250          options = install;
    251          break;
    252        }
    253      }
    254      if (!this.canInstallAddon(addon, options)) {
    255        ourResult.skipped.push(addon.id);
    256        continue;
    257      }
    258 
    259      // We can go ahead and attempt to install it.
    260      toInstall.push(addon);
    261 
    262      // We should always be able to QI the nsIURI to nsIURL. If not, we
    263      // still try to install the add-on, but we don't rewrite the URL,
    264      // potentially skewing metrics.
    265      try {
    266        addon.sourceURI.QueryInterface(Ci.nsIURL);
    267      } catch (ex) {
    268        this._log.warn(
    269          "Unable to QI sourceURI to nsIURL: " + addon.sourceURI.spec
    270        );
    271        continue;
    272      }
    273 
    274      let params = addon.sourceURI.query
    275        .split("&")
    276        .map(function rewrite(param) {
    277          if (param.indexOf("src=") == 0) {
    278            return "src=sync";
    279          }
    280          return param;
    281        });
    282 
    283      addon.sourceURI = addon.sourceURI
    284        .mutate()
    285        .setQuery(params.join("&"))
    286        .finalize();
    287    }
    288 
    289    if (!toInstall.length) {
    290      return ourResult;
    291    }
    292 
    293    const installPromises = [];
    294    // Start all the installs asynchronously. They will report back to us
    295    // as they finish, eventually triggering the global callback.
    296    for (let addon of toInstall) {
    297      let options = {};
    298      for (let install of installs) {
    299        if (install.id == addon.id) {
    300          options = install;
    301          break;
    302        }
    303      }
    304 
    305      installPromises.push(
    306        (async () => {
    307          try {
    308            const result = await this.installAddonFromSearchResult(
    309              addon,
    310              options
    311            );
    312            ourResult.installedIDs.push(result.id);
    313            ourResult.installs.push(result.install);
    314            ourResult.addons.push(result.addon);
    315          } catch (error) {
    316            ourResult.errors.push(error);
    317          }
    318        })()
    319      );
    320    }
    321 
    322    await Promise.all(installPromises);
    323 
    324    if (ourResult.errors.length) {
    325      throw new Error("1 or more add-ons failed to install");
    326    }
    327    return ourResult;
    328  },
    329 
    330  /**
    331   * Returns true if we are able to install the specified addon, false
    332   * otherwise. It is expected that this will log the reason if it returns
    333   * false.
    334   *
    335   * @param addon
    336   *        (Addon) Add-on instance to check.
    337   * @param options
    338   *        (object) The options specified for this addon. See installAddons()
    339   *        for the valid elements.
    340   */
    341  canInstallAddon(addon, options) {
    342    // sourceURI presence isn't enforced by AddonRepository. So, we skip
    343    // add-ons without a sourceURI.
    344    if (!addon.sourceURI) {
    345      this._log.info(
    346        "Skipping install of add-on because missing sourceURI: " + addon.id
    347      );
    348      return false;
    349    }
    350    // Verify that the source URI uses TLS. We don't allow installs from
    351    // insecure sources for security reasons. The Addon Manager ensures
    352    // that cert validation etc is performed.
    353    // (We should also consider just dropping this entirely and calling
    354    // XPIProvider.isInstallAllowed, but that has additional semantics we might
    355    // need to think through...)
    356    let requireSecureURI = true;
    357    if (options && options.requireSecureURI !== undefined) {
    358      requireSecureURI = options.requireSecureURI;
    359    }
    360 
    361    if (requireSecureURI) {
    362      let scheme = addon.sourceURI.scheme;
    363      if (scheme != "https") {
    364        this._log.info(
    365          `Skipping install of add-on "${addon.id}" because sourceURI's scheme of "${scheme}" is not trusted`
    366        );
    367        return false;
    368      }
    369    }
    370 
    371    // Policy prevents either installing this addon or any addon
    372    if (
    373      Services.policies &&
    374      (!Services.policies.mayInstallAddon(addon) ||
    375        !Services.policies.isAllowed("xpinstall"))
    376    ) {
    377      this._log.info(
    378        `Skipping install of "${addon.id}" due to enterprise policy`
    379      );
    380      return false;
    381    }
    382 
    383    this._log.info(`Add-on "${addon.id}" is able to be installed`);
    384    return true;
    385  },
    386 
    387  /**
    388   * Update the user disabled flag for an add-on.
    389   *
    390   * If the new flag matches the existing or if the add-on
    391   * isn't currently active, the function will return immediately.
    392   *
    393   * @param addon
    394   *        (Addon) Add-on instance to operate on.
    395   * @param value
    396   *        (bool) New value for add-on's userDisabled property.
    397   */
    398  updateUserDisabled(addon, value) {
    399    if (addon.userDisabled == value) {
    400      return;
    401    }
    402 
    403    this._log.info("Updating userDisabled flag: " + addon.id + " -> " + value);
    404    if (value) {
    405      addon.disable();
    406    } else {
    407      addon.enable();
    408    }
    409  },
    410 };
    411 
    412 export const AddonUtils = new AddonUtilsInternal();