tor-browser

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

ShellService.sys.mjs (20344B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 const lazy = {};
      9 
     10 ChromeUtils.defineESModuleGetters(lazy, {
     11  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     12  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
     13 });
     14 
     15 XPCOMUtils.defineLazyServiceGetter(
     16  lazy,
     17  "XreDirProvider",
     18  "@mozilla.org/xre/directory-provider;1",
     19  Ci.nsIXREDirProvider
     20 );
     21 
     22 XPCOMUtils.defineLazyServiceGetter(
     23  lazy,
     24  "BackgroundTasks",
     25  "@mozilla.org/backgroundtasks;1",
     26  Ci.nsIBackgroundTasks
     27 );
     28 
     29 ChromeUtils.defineLazyGetter(lazy, "log", () => {
     30  let { ConsoleAPI } = ChromeUtils.importESModule(
     31    "resource://gre/modules/Console.sys.mjs"
     32  );
     33  let consoleOptions = {
     34    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
     35    // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
     36    maxLogLevel: "error",
     37    maxLogLevelPref: "browser.shell.loglevel",
     38    prefix: "ShellService",
     39  };
     40  return new ConsoleAPI(consoleOptions);
     41 });
     42 
     43 const MSIX_PREVIOUSLY_PINNED_PREF =
     44  "browser.startMenu.msixPinnedWhenLastChecked";
     45 
     46 /**
     47 * Internal functionality to save and restore the docShell.allow* properties.
     48 */
     49 let ShellServiceInternal = {
     50  /**
     51   * Used to determine whether or not to offer "Set as desktop background"
     52   * functionality. Even if shell service is available it is not
     53   * guaranteed that it is able to set the background for every desktop
     54   * which is especially true for Linux with its many different desktop
     55   * environments.
     56   */
     57  get canSetDesktopBackground() {
     58    if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
     59      return true;
     60    }
     61 
     62    if (AppConstants.platform == "linux") {
     63      if (this.shellService) {
     64        let linuxShellService = this.shellService.QueryInterface(
     65          Ci.nsIGNOMEShellService
     66        );
     67        return linuxShellService.canSetDesktopBackground;
     68      }
     69    }
     70 
     71    return false;
     72  },
     73 
     74  /**
     75   * Used to determine based on the creation date of the home folder how old a
     76   * user profile is (and NOT the browser profile).
     77   */
     78  async getOSUserProfileAgeInDays() {
     79    let currentDate = new Date();
     80    let homeFolderCreationDate = new Date(
     81      (
     82        await IOUtils.stat(Services.dirsvc.get("Home", Ci.nsIFile).path)
     83      ).creationTime
     84    );
     85    // Round and return the age (=difference between today and creation) to a
     86    // resolution of days.
     87    return Math.round(
     88      (currentDate - homeFolderCreationDate) /
     89        1000 / // ms
     90        60 / // sec
     91        60 / // min
     92        24 // hours
     93    );
     94  },
     95 
     96  /**
     97   * Used to determine whether or not to show a "Set Default Browser"
     98   * query dialog. This attribute is true if the application is starting
     99   * up and "browser.shell.checkDefaultBrowser" is true, otherwise it
    100   * is false.
    101   */
    102  _checkedThisSession: false,
    103  get shouldCheckDefaultBrowser() {
    104    // If we've already checked, the browser has been started and this is a
    105    // new window open, and we don't want to check again.
    106    if (this._checkedThisSession) {
    107      return false;
    108    }
    109 
    110    if (!Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser")) {
    111      return false;
    112    }
    113 
    114    return true;
    115  },
    116 
    117  set shouldCheckDefaultBrowser(shouldCheck) {
    118    Services.prefs.setBoolPref(
    119      "browser.shell.checkDefaultBrowser",
    120      !!shouldCheck
    121    );
    122  },
    123 
    124  isDefaultBrowser(startupCheck, forAllTypes) {
    125    // If this is the first browser window, maintain internal state that we've
    126    // checked this session (so that subsequent window opens don't show the
    127    // default browser dialog).
    128    if (startupCheck) {
    129      this._checkedThisSession = true;
    130    }
    131    if (this.shellService) {
    132      return this.shellService.isDefaultBrowser(forAllTypes);
    133    }
    134    return false;
    135  },
    136 
    137  /**
    138   * Check if UserChoice is impossible.
    139   *
    140   * Separated for easy stubbing in tests.
    141   *
    142   * @returns {string}
    143   *   Telemetry result like "Err*", or null if UserChoice is possible.
    144   */
    145  _userChoiceImpossibleTelemetryResult() {
    146    let winShellService = this.shellService.QueryInterface(
    147      Ci.nsIWindowsShellService
    148    );
    149    if (!winShellService.checkAllProgIDsExist()) {
    150      return "ErrProgID";
    151    }
    152    if (!winShellService.checkBrowserUserChoiceHashes()) {
    153      return "ErrHash";
    154    }
    155    return null;
    156  },
    157 
    158  /**
    159   * Accommodate `setDefaultPDFHandlerOnlyReplaceBrowsers` feature.
    160   *
    161   * @returns {boolean}
    162   *   True if Firefox should set itself as default PDF handler, false otherwise.
    163   */
    164  _shouldSetDefaultPDFHandler() {
    165    if (
    166      !lazy.NimbusFeatures.shellService.getVariable(
    167        "setDefaultPDFHandlerOnlyReplaceBrowsers"
    168      )
    169    ) {
    170      return true;
    171    }
    172 
    173    const handler = this.getDefaultPDFHandler();
    174    if (handler === null) {
    175      // We only get an exception when something went really wrong.  Fail
    176      // safely: don't set Firefox as default PDF handler.
    177      lazy.log.warn(
    178        "Could not determine default PDF handler: not setting Firefox as " +
    179          "default PDF handler!"
    180      );
    181      return false;
    182    }
    183 
    184    if (!handler.registered) {
    185      lazy.log.debug(
    186        "Current default PDF handler has no registered association; " +
    187          "should set as default PDF handler."
    188      );
    189      return true;
    190    }
    191 
    192    if (handler.knownBrowser) {
    193      lazy.log.debug(
    194        "Current default PDF handler progID matches known browser; should " +
    195          "set as default PDF handler."
    196      );
    197      return true;
    198    }
    199 
    200    lazy.log.debug(
    201      "Current default PDF handler progID does not match known browser " +
    202        "prefix; should not set as default PDF handler."
    203    );
    204    return false;
    205  },
    206 
    207  getDefaultPDFHandler() {
    208    const knownBrowserPrefixes = [
    209      "AppXq0fevzme2pys62n3e0fbqa7peapykr8v", // Edge before Blink, per https://stackoverflow.com/a/32724723.
    210      "AppXd4nrz8ff68srnhf9t5a8sbjyar1cr723", // Another pre-Blink Edge identifier. See Bug 1858729.
    211      "Brave", // For "BraveFile".
    212      "Chrome", // For "ChromeHTML".
    213      "Firefox", // For "FirefoxHTML-*" or "FirefoxPDF-*".  Need to take from other installations of Firefox!
    214      "IE", // Best guess.
    215      "MSEdge", // For "MSEdgePDF".  Edgium.
    216      "Opera", // For "OperaStable", presumably varying with channel.
    217      "Yandex", // For "YandexPDF.IHKFKZEIOKEMR6BGF62QXCRIKM", presumably varying with installation.
    218    ];
    219 
    220    let currentProgID = "";
    221    try {
    222      // Returns the empty string when no association is registered, in
    223      // which case the prefix matching will fail and we'll set Firefox as
    224      // the default PDF handler.
    225      currentProgID = this.queryCurrentDefaultHandlerFor(".pdf");
    226    } catch (e) {
    227      // We only get an exception when something went really wrong.  Fail
    228      // safely: don't set Firefox as default PDF handler.
    229      lazy.log.warn("Failed to queryCurrentDefaultHandlerFor:");
    230      return null;
    231    }
    232 
    233    if (currentProgID == "") {
    234      return { registered: false, knownBrowser: false };
    235    }
    236 
    237    const knownBrowserPrefix = knownBrowserPrefixes.find(it =>
    238      currentProgID.startsWith(it)
    239    );
    240 
    241    if (knownBrowserPrefix) {
    242      lazy.log.debug(`Found known browser prefix: ${knownBrowserPrefix}`);
    243    }
    244 
    245    return {
    246      registered: true,
    247      knownBrowser: !!knownBrowserPrefix,
    248    };
    249  },
    250 
    251  /**
    252   * Set the default browser through the UserChoice registry keys on Windows.
    253   *
    254   * NOTE: This does NOT open the System Settings app for manual selection
    255   * in case of failure. If that is desired, catch the exception and call
    256   * setDefaultBrowser().
    257   *
    258   * @returns {Promise<void>}
    259   *   Resolves when successful, rejects with Error on failure.
    260   */
    261  async setAsDefaultUserChoice() {
    262    if (AppConstants.platform != "win") {
    263      throw new Error("Windows-only");
    264    }
    265 
    266    lazy.log.info("Setting Firefox as default using UserChoice");
    267 
    268    let telemetryResult = "ErrOther";
    269 
    270    try {
    271      telemetryResult =
    272        this._userChoiceImpossibleTelemetryResult() ?? "ErrOther";
    273      if (telemetryResult == "ErrProgID") {
    274        throw new Error("checkAllProgIDsExist() failed");
    275      }
    276      if (telemetryResult == "ErrHash") {
    277        throw new Error("checkBrowserUserChoiceHashes() failed");
    278      }
    279 
    280      const aumi = lazy.XreDirProvider.getInstallHash();
    281 
    282      telemetryResult = "ErrLaunchExe";
    283      const extraFileExtensions = [];
    284      if (
    285        lazy.NimbusFeatures.shellService.getVariable("setDefaultPDFHandler")
    286      ) {
    287        if (this._shouldSetDefaultPDFHandler()) {
    288          lazy.log.info("Setting Firefox as default PDF handler");
    289          extraFileExtensions.push(".pdf", "FirefoxPDF");
    290        } else {
    291          lazy.log.info("Not setting Firefox as default PDF handler");
    292        }
    293      }
    294      try {
    295        await this.defaultAgent.setDefaultBrowserUserChoiceAsync(
    296          aumi,
    297          extraFileExtensions
    298        );
    299      } catch (err) {
    300        telemetryResult = "ErrOther";
    301        this._handleWDBAResult(err.result || Cr.NS_ERROR_FAILURE);
    302      }
    303      telemetryResult = "Success";
    304    } catch (ex) {
    305      if (ex instanceof WDBAError) {
    306        telemetryResult = ex.telemetryResult;
    307      }
    308 
    309      throw ex;
    310    } finally {
    311      Glean.browser.setDefaultUserChoiceResult[telemetryResult].add(1);
    312    }
    313  },
    314 
    315  async setAsDefaultPDFHandlerUserChoice() {
    316    if (AppConstants.platform != "win") {
    317      throw new Error("Windows-only");
    318    }
    319 
    320    let telemetryResult = "ErrOther";
    321 
    322    try {
    323      const aumi = lazy.XreDirProvider.getInstallHash();
    324      try {
    325        this.defaultAgent.setDefaultExtensionHandlersUserChoice(aumi, [
    326          ".pdf",
    327          "FirefoxPDF",
    328        ]);
    329      } catch (err) {
    330        telemetryResult = "ErrOther";
    331        this._handleWDBAResult(err.result || Cr.NS_ERROR_FAILURE);
    332      }
    333      telemetryResult = "Success";
    334    } catch (ex) {
    335      if (ex instanceof WDBAError) {
    336        telemetryResult = ex.telemetryResult;
    337      }
    338 
    339      throw ex;
    340    } finally {
    341      Glean.browser.setDefaultPdfHandlerUserChoiceResult[telemetryResult].add(
    342        1
    343      );
    344    }
    345  },
    346 
    347  async _maybeShowSetDefaultGuidanceNotification() {
    348    if (
    349      lazy.NimbusFeatures.shellService.getVariable(
    350        "setDefaultGuidanceNotifications"
    351      ) &&
    352      // Disable showing toast notification from Firefox Background Tasks.
    353      !lazy.BackgroundTasks?.isBackgroundTaskMode
    354    ) {
    355      await lazy.ASRouter.waitForInitialized;
    356      const win = Services.wm.getMostRecentBrowserWindow() ?? null;
    357      lazy.ASRouter.sendTriggerMessage({
    358        browser: win,
    359        id: "deeplinkedToWindowsSettingsUI",
    360      });
    361    }
    362  },
    363 
    364  // override nsIShellService.setDefaultBrowser() on the ShellService proxy.
    365  async setDefaultBrowser(forAllUsers) {
    366    // On Windows, our best chance is to set UserChoice, so try that first.
    367    if (
    368      AppConstants.platform == "win" &&
    369      Services.prefs.getBoolPref("browser.shell.setDefaultBrowserUserChoice")
    370    ) {
    371      try {
    372        await this.setAsDefaultUserChoice();
    373        return;
    374      } catch (err) {
    375        lazy.log.warn(
    376          "Error thrown during setAsDefaultUserChoice. Full exception:",
    377          err
    378        );
    379 
    380        // intentionally fall through to setting via the non-user choice pathway on error
    381      }
    382    }
    383 
    384    this.shellService.setDefaultBrowser(forAllUsers);
    385    this._maybeShowSetDefaultGuidanceNotification();
    386  },
    387 
    388  async setAsDefault() {
    389    let setAsDefaultError = false;
    390    try {
    391      await ShellService.setDefaultBrowser(false);
    392    } catch (ex) {
    393      setAsDefaultError = true;
    394      console.error(ex);
    395    }
    396    // Here isUserDefault and setUserDefaultError appear
    397    // to be inverse of each other, but that is only because this function is
    398    // called when the browser is set as the default. During startup we record
    399    // the isUserDefault value without recording setUserDefaultError.
    400    Glean.browser.isUserDefault[!setAsDefaultError ? "true" : "false"].add();
    401    Glean.browser.setDefaultError[setAsDefaultError ? "true" : "false"].add();
    402  },
    403 
    404  setAsDefaultPDFHandler(onlyIfKnownBrowser = false) {
    405    if (onlyIfKnownBrowser && !this.getDefaultPDFHandler().knownBrowser) {
    406      return;
    407    }
    408 
    409    if (AppConstants.platform == "win") {
    410      this.setAsDefaultPDFHandlerUserChoice();
    411    }
    412  },
    413 
    414  /**
    415   * Determine if we're the default handler for the given file extension (like
    416   * ".pdf") or protocol (like "https").  Windows-only for now.
    417   *
    418   * @returns {boolean} true if we are the default handler, false otherwise.
    419   */
    420  isDefaultHandlerFor(aFileExtensionOrProtocol) {
    421    if (AppConstants.platform == "win") {
    422      return this.shellService
    423        .QueryInterface(Ci.nsIWindowsShellService)
    424        .isDefaultHandlerFor(aFileExtensionOrProtocol);
    425    }
    426    return false;
    427  },
    428 
    429  /**
    430   * Checks if Firefox app can and isn't pinned to OS "taskbar."
    431   *
    432   * @throws if not called from main process.
    433   */
    434  async doesAppNeedPin(privateBrowsing = false) {
    435    if (
    436      Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT
    437    ) {
    438      throw new Components.Exception(
    439        "Can't determine pinned from child process",
    440        Cr.NS_ERROR_NOT_AVAILABLE
    441      );
    442    }
    443 
    444    // Pretend pinning is not needed/supported if remotely disabled.
    445    if (lazy.NimbusFeatures.shellService.getVariable("disablePin")) {
    446      return false;
    447    }
    448 
    449    // Bug 1758770: Pinning private browsing on MSIX is currently
    450    // not possible.
    451    if (
    452      privateBrowsing &&
    453      AppConstants.platform === "win" &&
    454      Services.sysinfo.getProperty("hasWinPackageId")
    455    ) {
    456      return false;
    457    }
    458 
    459    // Currently this only works on certain Windows versions.
    460    try {
    461      // First check if we can even pin the app where an exception means no.
    462      await this.shellService
    463        .QueryInterface(Ci.nsIWindowsShellService)
    464        .checkPinCurrentAppToTaskbarAsync(privateBrowsing);
    465      let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(
    466        Ci.nsIWinTaskbar
    467      );
    468 
    469      // Then check if we're already pinned.
    470      return !(await this.shellService.isCurrentAppPinnedToTaskbarAsync(
    471        privateBrowsing
    472          ? winTaskbar.defaultPrivateGroupId
    473          : winTaskbar.defaultGroupId
    474      ));
    475    } catch (ex) {}
    476 
    477    // Next check mac pinning to dock.
    478    try {
    479      // Accessing this.macDockSupport will ensure we're actually running
    480      // on Mac (it's possible to be on Linux in this block).
    481      const isInDock = this.macDockSupport.isAppInDock;
    482      // We can't pin Private Browsing mode on Mac, only a shortcut to the vanilla app
    483      return privateBrowsing ? false : !isInDock;
    484    } catch (ex) {}
    485    return false;
    486  },
    487 
    488  /**
    489   * Pin Firefox app to the OS "taskbar."
    490   */
    491  async pinToTaskbar(privateBrowsing = false) {
    492    if (await this.doesAppNeedPin(privateBrowsing)) {
    493      try {
    494        if (AppConstants.platform == "win") {
    495          await this.shellService.pinCurrentAppToTaskbarAsync(privateBrowsing);
    496        } else if (AppConstants.platform == "macosx") {
    497          this.macDockSupport.ensureAppIsPinnedToDock();
    498        }
    499      } catch (ex) {
    500        console.error(ex);
    501      }
    502    }
    503  },
    504 
    505  /**
    506   * On MSIX builds, pins Firefox to the Windows Start Menu
    507   *
    508   * On non-MSIX builds, this function is a no-op and always returns false.
    509   *
    510   * @returns {boolean} true if we successfully pin and false otherwise.
    511   */
    512  async pinToStartMenu() {
    513    if (await this.doesAppNeedStartMenuPin()) {
    514      try {
    515        let pinSuccess =
    516          await this.shellService.pinCurrentAppToStartMenuAsync(false);
    517        Services.prefs.setBoolPref(MSIX_PREVIOUSLY_PINNED_PREF, pinSuccess);
    518        return pinSuccess;
    519      } catch (err) {
    520        lazy.log.warn("Error thrown during pinCurrentAppToStartMenuAsync", err);
    521        Services.prefs.setBoolPref(MSIX_PREVIOUSLY_PINNED_PREF, false);
    522      }
    523    }
    524    return false;
    525  },
    526 
    527  /**
    528   * On MSIX builds, checks if Firefox app can be and is not
    529   * pinned to the Windows Start Menu.
    530   *
    531   * On non-MSIX builds, this function is a no-op and always returns false.
    532   *
    533   * @returns {boolean} true if this is an MSIX install and we are not yet
    534   *                    pinned to the Start Menu.
    535   *
    536   * @throws if not called from main process.
    537   */
    538  async doesAppNeedStartMenuPin() {
    539    if (
    540      Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT
    541    ) {
    542      throw new Components.Exception(
    543        "Can't determine pinned from child process",
    544        Cr.NS_ERROR_NOT_AVAILABLE
    545      );
    546    }
    547    if (
    548      Services.prefs.getBoolPref("browser.shell.disableStartMenuPin", false)
    549    ) {
    550      return false;
    551    }
    552    try {
    553      return (
    554        AppConstants.platform === "win" &&
    555        Services.sysinfo.getProperty("hasWinPackageId") &&
    556        !(await this.shellService.isCurrentAppPinnedToStartMenuAsync())
    557      );
    558    } catch (ex) {}
    559    return false;
    560  },
    561 
    562  /**
    563   * On MSIX builds, checks if Firefox is no longer pinned to
    564   * the Windows Start Menu when it previously was and records
    565   * a Glean event if so.
    566   *
    567   * On non-MSIX builds, this function is a no-op.
    568   */
    569  async recordWasPreviouslyPinnedToStartMenu() {
    570    if (!Services.sysinfo.getProperty("hasWinPackageId")) {
    571      return;
    572    }
    573    let isPinned = await this.shellService.isCurrentAppPinnedToStartMenuAsync();
    574    if (
    575      !isPinned &&
    576      Services.prefs.getBoolPref(MSIX_PREVIOUSLY_PINNED_PREF, false)
    577    ) {
    578      Services.prefs.setBoolPref(MSIX_PREVIOUSLY_PINNED_PREF, isPinned);
    579      Glean.startMenu.manuallyUnpinnedSinceLastLaunch.record();
    580    }
    581  },
    582 
    583  _handleWDBAResult(exitCode) {
    584    if (exitCode != Cr.NS_OK) {
    585      const telemetryResult =
    586        new Map([
    587          [Cr.NS_ERROR_WDBA_NO_PROGID, "ErrExeProgID"],
    588          [Cr.NS_ERROR_WDBA_HASH_CHECK, "ErrExeHash"],
    589          [Cr.NS_ERROR_WDBA_REJECTED, "ErrExeRejected"],
    590          [Cr.NS_ERROR_WDBA_BUILD, "ErrBuild"],
    591        ]).get(exitCode) ?? "ErrExeOther";
    592 
    593      throw new WDBAError(exitCode, telemetryResult);
    594    }
    595  },
    596 };
    597 
    598 // Functions may be present or absent dependent on whether the `nsIShellService`
    599 // has been queried for the interface implementing it, as querying the interface
    600 // adds it's functions to the queried JS object. Coincidental querying is more
    601 // likely to occur for Firefox Desktop than a Firefox Background Task. To force
    602 // consistent behavior, we query the native shell interface inheriting from
    603 // `nsIShellService` on setup.
    604 let shellInterface;
    605 switch (AppConstants.platform) {
    606  case "win":
    607    shellInterface = Ci.nsIWindowsShellService;
    608    break;
    609  case "macosx":
    610    shellInterface = Ci.nsIMacShellService;
    611    break;
    612  case "linux":
    613    shellInterface = Ci.nsIGNOMEShellService;
    614    break;
    615  default:
    616    lazy.log.warn(
    617      `No platform native shell service interface for ${AppConstants.platform} queried, add for new platforms.`
    618    );
    619    shellInterface = Ci.nsIShellService;
    620 }
    621 
    622 XPCOMUtils.defineLazyServiceGetters(ShellServiceInternal, {
    623  defaultAgent: ["@mozilla.org/default-agent;1", Ci.nsIDefaultAgent],
    624  shellService: ["@mozilla.org/browser/shell-service;1", shellInterface],
    625  macDockSupport: [
    626    "@mozilla.org/widget/macdocksupport;1",
    627    Ci.nsIMacDockSupport,
    628  ],
    629 });
    630 
    631 /**
    632 * The external API exported by this module.
    633 */
    634 export var ShellService = new Proxy(ShellServiceInternal, {
    635  get(target, name) {
    636    if (name in target) {
    637      return target[name];
    638    }
    639    // n.b. If a native shell interface member is not present on `shellService`,
    640    // it may be necessary to query the native interface.
    641    if (target.shellService && name in target.shellService) {
    642      return target.shellService[name];
    643    }
    644    lazy.log.warn(
    645      `${name.toString()} not found in ShellService: ${target.shellService}`
    646    );
    647    return undefined;
    648  },
    649 });
    650 
    651 class WDBAError extends Error {
    652  constructor(exitCode, telemetryResult) {
    653    super(`WDBA nonzero exit code ${exitCode}: ${telemetryResult}`);
    654 
    655    this.exitCode = exitCode;
    656    this.telemetryResult = telemetryResult;
    657  }
    658 }