tor-browser

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

WebProtocolHandlerRegistrar.sys.mjs (24114B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 const STRING_BUNDLE_URI = "chrome://browser/locale/feeds/subscribe.properties";
      9 
     10 export function WebProtocolHandlerRegistrar() {}
     11 
     12 const lazy = {};
     13 
     14 XPCOMUtils.defineLazyServiceGetters(lazy, {
     15  ExternalProtocolService: [
     16    "@mozilla.org/uriloader/external-protocol-service;1",
     17    Ci.nsIExternalProtocolService,
     18  ],
     19 });
     20 
     21 ChromeUtils.defineESModuleGetters(lazy, {
     22  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     23  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     24 });
     25 
     26 ChromeUtils.defineLazyGetter(lazy, "log", () => {
     27  let { ConsoleAPI } = ChromeUtils.importESModule(
     28    "resource://gre/modules/Console.sys.mjs"
     29  );
     30  let consoleOptions = {
     31    // tip: set maxLogLevel to "debug" and use lazy.log.debug() to create
     32    // detailed messages during development. See LOG_LEVELS in Console.sys.mjs
     33    // for details.
     34    maxLogLevel: "warning",
     35    maxLogLevelPref: "browser.protocolhandler.loglevel",
     36    prefix: "WebProtocolHandlerRegistrar.sys.mjs",
     37  };
     38  return new ConsoleAPI(consoleOptions);
     39 });
     40 
     41 WebProtocolHandlerRegistrar.prototype = {
     42  get stringBundle() {
     43    let sb = Services.strings.createBundle(STRING_BUNDLE_URI);
     44    delete WebProtocolHandlerRegistrar.prototype.stringBundle;
     45    return (WebProtocolHandlerRegistrar.prototype.stringBundle = sb);
     46  },
     47 
     48  _getFormattedString(key, params) {
     49    return this.stringBundle.formatStringFromName(key, params);
     50  },
     51 
     52  _getString(key) {
     53    return this.stringBundle.GetStringFromName(key);
     54  },
     55 
     56  /* Because we want to iterate over the known webmailers in the observe method
     57   * and with each site visited, we want to check as fast as possible if the
     58   * current site is already registered as a mailto handler. Using the sites
     59   * domain name as a key ensures that we can use Map.has(...) later to find
     60   * it.
     61   */
     62  _addedObservers: 0,
     63  _knownWebmailerCache: new Map(),
     64  _ensureWebmailerCache() {
     65    this._knownWebmailerCache = new Map();
     66 
     67    const handler =
     68      lazy.ExternalProtocolService.getProtocolHandlerInfo("mailto");
     69 
     70    for (const h of handler.possibleApplicationHandlers.enumerate()) {
     71      // Services.io.newURI could fail for broken handlers in which case we
     72      // simply leave them out, but write a debug message (just in case)
     73      try {
     74        if (h instanceof Ci.nsIWebHandlerApp && h.uriTemplate) {
     75          const mailerUri = Services.io.newURI(h.uriTemplate);
     76          if (mailerUri.scheme == "https") {
     77            this._knownWebmailerCache.set(
     78              Services.io.newURI(h.uriTemplate).host,
     79              {
     80                uriPath: Services.io.newURI(h.uriTemplate).resolve("."),
     81                uriTemplate: Services.io.newURI(h.uriTemplate),
     82                name: h.name,
     83              }
     84            );
     85          }
     86        }
     87      } catch (e) {
     88        lazy.log.debug(`Could not add ${h.uriTemplate} to cache: ${e.message}`);
     89      }
     90    }
     91  },
     92 
     93  /**
     94   * This function can be called multiple times and (re-)initializes the cache
     95   * if the feature is toggled on. If called with the feature off it will also
     96   * unregister the observers.
     97   *
     98   * @param {boolean} firstInit
     99   */
    100  init(firstInit = false) {
    101    if (firstInit) {
    102      lazy.NimbusFeatures.mailto.onUpdate(() =>
    103        // make firstInit explicitly false to avoid multiple registrations.
    104        this.init(false)
    105      );
    106    }
    107 
    108    const observers = ["mailto::onLocationChange", "mailto::onClearCache"];
    109    if (
    110      lazy.NimbusFeatures.mailto.getVariable("dualPrompt") &&
    111      lazy.NimbusFeatures.mailto.getVariable("dualPrompt.onLocationChange")
    112    ) {
    113      this._ensureWebmailerCache();
    114      // Make sure, that our local observers are never registered twice:
    115      if (0 == this._addedObservers) {
    116        observers.forEach(o => {
    117          this._addedObservers++;
    118          Services.obs.addObserver(this, o);
    119        });
    120        lazy.log.debug(`mailto observers activated: [${observers}]`);
    121      }
    122    } else {
    123      // With `dualPrompt` and `dualPrompt.onLocationChange` toggled on we get
    124      // up to two notifications when we turn the feature off again, but we
    125      // only want to unregister the observers once.
    126      //
    127      // Using `hasObservers` would allow us to loop over all observers as long
    128      // as there are more, but hasObservers is not implemented hence why we
    129      // use `enumerateObservers` here to create the loop and `hasMoreElements`
    130      // to return true or false as `hasObservers` would if it existed.
    131      observers.forEach(o => {
    132        if (
    133          0 < this._addedObservers &&
    134          Services.obs.enumerateObservers(o).hasMoreElements()
    135        ) {
    136          Services.obs.removeObserver(this, o);
    137          this._addedObservers--;
    138          lazy.log.debug(`mailto observer "${o}" deactivated.`);
    139        }
    140      });
    141    }
    142  },
    143 
    144  async observe(aBrowser, aTopic) {
    145    try {
    146      switch (aTopic) {
    147        case "mailto::onLocationChange": {
    148          // registerProtocolHandler only works for https
    149          const uri = aBrowser.currentURI;
    150          if (!uri?.schemeIs("https")) {
    151            return;
    152          }
    153 
    154          const host = uri.host;
    155          if (this._knownWebmailerCache.has(host)) {
    156            // second: search the cache for an entry which starts with the path
    157            // of the current uri. If it exists we identified the current page as
    158            // webmailer (again).
    159            const value = this._knownWebmailerCache.get(host);
    160            this._askUserToSetMailtoHandler(
    161              aBrowser,
    162              "mailto",
    163              value.uriTemplate,
    164              value.name
    165            );
    166          }
    167          break; // the switch(topic) statement
    168        }
    169        case "mailto::onClearCache":
    170          // clear the cache for now. We could try to dynamically update the
    171          // cache, which is easy if a webmailer is added to the settings, but
    172          // becomes more complicated when webmailers are removed, because then
    173          // the store gets rewritten and we would require an event to deal with
    174          // that as well. So instead we recreate it entirely.
    175          this._ensureWebmailerCache();
    176          break;
    177        default:
    178          lazy.log.debug(`observe reached with unknown topic: ${aTopic}`);
    179      }
    180    } catch (e) {
    181      lazy.log.debug(`Problem in observer: ${e}`);
    182    }
    183  },
    184 
    185  /**
    186   * See nsIWebProtocolHandlerRegistrar
    187   */
    188  removeProtocolHandler(aProtocol, aURITemplate) {
    189    let handlerInfo =
    190      lazy.ExternalProtocolService.getProtocolHandlerInfo(aProtocol);
    191    let handlers = handlerInfo.possibleApplicationHandlers;
    192    for (let i = 0; i < handlers.length; i++) {
    193      try {
    194        // We only want to test web handlers
    195        let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
    196        if (handler.uriTemplate == aURITemplate) {
    197          handlers.removeElementAt(i);
    198          let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
    199            Ci.nsIHandlerService
    200          );
    201          hs.store(handlerInfo);
    202          return;
    203        }
    204      } catch (e) {
    205        /* it wasn't a web handler */
    206      }
    207    }
    208  },
    209 
    210  /**
    211   * Determines if a web handler is already registered.
    212   *
    213   * @param {string} aProtocol
    214   *        The scheme of the web handler we are checking for.
    215   * @param {string} aURITemplate
    216   *        The URI template that the handler uses to handle the protocol.
    217   * @returns {boolean} true if it is already registered, false otherwise.
    218   */
    219  _protocolHandlerRegistered(aProtocol, aURITemplate) {
    220    let handlerInfo =
    221      lazy.ExternalProtocolService.getProtocolHandlerInfo(aProtocol);
    222    let handlers = handlerInfo.possibleApplicationHandlers;
    223    for (let handler of handlers.enumerate()) {
    224      // We only want to test web handlers
    225      if (
    226        handler instanceof Ci.nsIWebHandlerApp &&
    227        handler.uriTemplate == aURITemplate
    228      ) {
    229        return true;
    230      }
    231    }
    232    return false;
    233  },
    234 
    235  /**
    236   * Returns true if aURITemplate.spec points to the currently configured
    237   * handler for aProtocol links and the OS default is also readily configured.
    238   * Returns false if some of it can be made default.
    239   *
    240   * @param {string} aProtocol
    241   *        The scheme of the web handler we are checking for.
    242   * @param {string} aURITemplate
    243   *        The URI template that the handler uses to handle the protocol.
    244   */
    245  _isProtocolHandlerDefault(aProtocol, aURITemplate) {
    246    const handlerInfo =
    247      lazy.ExternalProtocolService.getProtocolHandlerInfo(aProtocol);
    248 
    249    if (
    250      handlerInfo.preferredAction == Ci.nsIHandlerInfo.useHelperApp &&
    251      handlerInfo.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp
    252    ) {
    253      let webHandlerApp =
    254        handlerInfo.preferredApplicationHandler.QueryInterface(
    255          Ci.nsIWebHandlerApp
    256        );
    257 
    258      // If we are already configured as default, we cannot set a new default
    259      // and if the current site is already registered as default webmailer we
    260      // are fully set up as the default app for webmail.
    261      if (
    262        !this._canSetOSDefault(aProtocol) &&
    263        webHandlerApp.uriTemplate == aURITemplate.spec
    264      ) {
    265        return true;
    266      }
    267    }
    268    return false;
    269  },
    270 
    271  /**
    272   * Private method to return the installHash, which is important for app
    273   * registration on OS level. Without it apps cannot be default/handler apps
    274   * under Windows. Because this value is used to check if its possible to reset
    275   * the default and to actually set it as well, this function puts the
    276   * acquisition of the installHash in one place in the hopes that check and set
    277   * conditions will never deviate.
    278   *
    279   * @returns {string} installHash
    280   */
    281  _getInstallHash() {
    282    const xreDirProvider = Cc[
    283      "@mozilla.org/xre/directory-provider;1"
    284    ].getService(Ci.nsIXREDirProvider);
    285    return xreDirProvider.getInstallHash();
    286  },
    287 
    288  /**
    289   * Private method to check if we are already the default protocolhandler
    290   * for `protocol`.
    291   *
    292   * @param {string} protocol name, e.g. mailto (without ://)
    293   * @returns {boolean}
    294   */
    295  _isOsDefault(protocol) {
    296    let shellService = Cc[
    297      "@mozilla.org/browser/shell-service;1"
    298    ].createInstance(Ci.nsIWindowsShellService);
    299 
    300    if (shellService.isDefaultHandlerFor(protocol)) {
    301      lazy.log.debug("_isOsDefault returns true.");
    302      return true;
    303    }
    304 
    305    lazy.log.debug("_isOsDefault returns false.");
    306    return false;
    307  },
    308 
    309  /**
    310   * Private method to determine if we can set a new OS default for a certain
    311   * protocol.
    312   *
    313   * @param {string} protocol name, e.g. mailto (without ://)
    314   * @returns {boolean}
    315   */
    316  _canSetOSDefault(protocol) {
    317    // an installHash is required for the association with a scheme handler,
    318    // also see _setOSDefault()
    319    if ("" == this._getInstallHash()) {
    320      lazy.log.debug("_canSetOSDefault returns false.");
    321      return false;
    322    }
    323 
    324    if (this._isOsDefault(protocol)) {
    325      return false;
    326    }
    327 
    328    return true;
    329  },
    330 
    331  /**
    332   * Private method to reset the OS default for a certain protocol/uri scheme.
    333   * We basically ignore, that setDefaultExtensionHandlersUserChoice can fail
    334   * when the installHash is wrong or cannot be determined.
    335   *
    336   * @param {string} protocol name, e.g. mailto (without ://)
    337   * @returns {boolean}
    338   */
    339  _setOSDefault(protocol) {
    340    try {
    341      let defaultAgent = Cc["@mozilla.org/default-agent;1"].createInstance(
    342        Ci.nsIDefaultAgent
    343      );
    344      defaultAgent.setDefaultExtensionHandlersUserChoice(
    345        this._getInstallHash(),
    346        [protocol, "FirefoxURL"]
    347      );
    348      return true;
    349    } catch (e) {
    350      // TODO: why could not we just add the installHash and promote the running
    351      // install to be a properly installed one?
    352      lazy.log.debug(
    353        "Could not set Firefox as default application for " +
    354          protocol +
    355          ", because: " +
    356          e.message
    357      );
    358    }
    359    return false;
    360  },
    361 
    362  /**
    363   * Private method to set the default uri to handle a certain protocol. This
    364   * automates in a way what a user can do in settings under applications,
    365   * where different 'actions' can be chosen for different 'content types'.
    366   *
    367   * @param {string} protocol
    368   * @param {handler} handler
    369   */
    370  _setProtocolHandlerDefault(protocol, handler) {
    371    let handlerInfo =
    372      lazy.ExternalProtocolService.getProtocolHandlerInfo(protocol);
    373    handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
    374    handlerInfo.preferredApplicationHandler = handler;
    375    handlerInfo.alwaysAskBeforeHandling = false;
    376    let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
    377      Ci.nsIHandlerService
    378    );
    379    hs.store(handlerInfo);
    380    return handlerInfo;
    381  },
    382 
    383  /**
    384   * Private method to add a ProtocolHandler of type nsIWebHandlerApp to the
    385   * list of possible handlers for a protocol.
    386   *
    387   * @param {string} protocol - e.g. 'mailto', so again without ://
    388   * @param {string} name - the protocol associated 'Action'
    389   * @param {string} uri - the uri (compare 'use other...' in the preferences)
    390   * @returns {handler} handler - either the existing one or a newly created
    391   */
    392  _addWebProtocolHandler(protocol, name, uri) {
    393    let phi = lazy.ExternalProtocolService.getProtocolHandlerInfo(protocol);
    394    // not adding duplicates and bail out with the existing entry
    395    for (let h of phi.possibleApplicationHandlers.enumerate()) {
    396      if (h instanceof Ci.nsIWebHandlerApp && h.uriTemplate === uri) {
    397        return h;
    398      }
    399    }
    400 
    401    let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
    402      Ci.nsIWebHandlerApp
    403    );
    404    handler.name = name;
    405    handler.uriTemplate = uri;
    406 
    407    let handlerInfo =
    408      lazy.ExternalProtocolService.getProtocolHandlerInfo(protocol);
    409    handlerInfo.possibleApplicationHandlers.appendElement(handler);
    410 
    411    let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
    412      Ci.nsIHandlerService
    413    );
    414    hs.store(handlerInfo);
    415 
    416    return handler;
    417  },
    418 
    419  /**
    420   * See nsIWebProtocolHandlerRegistrar
    421   */
    422  registerProtocolHandler(
    423    aProtocol,
    424    aURI,
    425    aTitle,
    426    aDocumentURI,
    427    aBrowserOrWindow
    428  ) {
    429    // first mitigation: check if the API call comes from another domain
    430    aProtocol = (aProtocol || "").toLowerCase();
    431    if (!aURI || !aDocumentURI) {
    432      return;
    433    }
    434 
    435    let browser = aBrowserOrWindow; // This is the e10s case.
    436    if (aBrowserOrWindow instanceof Ci.nsIDOMWindow) {
    437      // In the non-e10s case, grab the browser off the same-process window.
    438      let rootDocShell = aBrowserOrWindow.docShell.sameTypeRootTreeItem;
    439      browser = rootDocShell.QueryInterface(Ci.nsIDocShell).chromeEventHandler;
    440    }
    441 
    442    let browserWindow = browser.ownerGlobal;
    443    try {
    444      browserWindow.navigator.checkProtocolHandlerAllowed(
    445        aProtocol,
    446        aURI,
    447        aDocumentURI
    448      );
    449    } catch (ex) {
    450      // We should have already shown the user an error.
    451      return;
    452    }
    453    if (lazy.NimbusFeatures.mailto.getVariable("dualPrompt")) {
    454      if ("mailto" === aProtocol) {
    455        lazy.NimbusFeatures.mailto.recordExposureEvent();
    456        this._askUserToSetMailtoHandler(browser, aProtocol, aURI, aTitle);
    457        return;
    458      }
    459    }
    460 
    461    // If the protocol handler is already registered, just return early.
    462    if (this._protocolHandlerRegistered(aProtocol, aURI.spec)) {
    463      return;
    464    }
    465 
    466    // Now Ask the user and provide the proper callback
    467    let message = this._getFormattedString("addProtocolHandlerMessage", [
    468      aURI.host,
    469      aProtocol,
    470    ]);
    471 
    472    let notificationIcon = aURI.prePath + "/favicon.ico";
    473    let notificationValue = "Protocol Registration: " + aProtocol;
    474    let addButton = {
    475      label: this._getString("addProtocolHandlerAddButton"),
    476      accessKey: this._getString("addProtocolHandlerAddButtonAccesskey"),
    477      protocolInfo: { protocol: aProtocol, uri: aURI.spec, name: aTitle },
    478      primary: true,
    479 
    480      callback(aNotification, aButtonInfo) {
    481        let protocol = aButtonInfo.protocolInfo.protocol;
    482        let name = aButtonInfo.protocolInfo.name;
    483 
    484        let handler = Cc[
    485          "@mozilla.org/uriloader/web-handler-app;1"
    486        ].createInstance(Ci.nsIWebHandlerApp);
    487        handler.name = name;
    488        handler.uriTemplate = aButtonInfo.protocolInfo.uri;
    489 
    490        let handlerInfo =
    491          lazy.ExternalProtocolService.getProtocolHandlerInfo(protocol);
    492        handlerInfo.possibleApplicationHandlers.appendElement(handler);
    493 
    494        // Since the user has agreed to add a new handler, chances are good
    495        // that the next time they see a handler of this type, they're going
    496        // to want to use it.  Reset the handlerInfo to ask before the next
    497        // use.
    498        handlerInfo.alwaysAskBeforeHandling = true;
    499 
    500        let hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
    501          Ci.nsIHandlerService
    502        );
    503        hs.store(handlerInfo);
    504      },
    505    };
    506 
    507    let notificationBox = browser.getTabBrowser().getNotificationBox(browser);
    508 
    509    // check if the notification box is already shown
    510    if (notificationBox.getNotificationWithValue(notificationValue)) {
    511      return;
    512    }
    513 
    514    notificationBox.appendNotification(
    515      notificationValue,
    516      {
    517        label: message,
    518        image: notificationIcon,
    519        priority: notificationBox.PRIORITY_INFO_LOW,
    520      },
    521      [addButton]
    522    );
    523  },
    524 
    525  /**
    526   * Special implementation for mailto: A prompt (notificationbox.js) is only
    527   * shown if there is a realistic chance that we can really set the OS default,
    528   * e.g. if we have been properly installed and the current page is not already
    529   * the default and we have not asked users too often the same question.
    530   *
    531   * @param {string} browser
    532   * @param {string} aProtocol
    533   * @param {nsIURI} aURI
    534   * @param {string} aTitle
    535   */
    536  async _askUserToSetMailtoHandler(browser, aProtocol, aURI, aTitle) {
    537    let notificationId = "OS Protocol Registration: " + aProtocol;
    538 
    539    // guard: we do not want to reconfigure settings in private browsing mode
    540    if (lazy.PrivateBrowsingUtils.isWindowPrivate(browser.ownerGlobal)) {
    541      lazy.log.debug("prompt not shown, because this is a private window.");
    542      return;
    543    }
    544 
    545    // guard: check if everything has been configured to use the current site
    546    // as default webmailer and bail out if so.
    547    if (this._isProtocolHandlerDefault(aProtocol, aURI)) {
    548      lazy.log.debug(
    549        `prompt not shown, because ${aTitle} is already configured to` +
    550          ` handle ${aProtocol}-links under ${aURI.spec}.`
    551      );
    552      return;
    553    }
    554 
    555    // guard: bail out if this site has been dismissed before (either by
    556    // clicking the 'x' button or the 'not now' button.
    557    let principal = browser.getTabBrowser().contentPrincipal;
    558    if (
    559      Ci.nsIPermissionManager.DENY_ACTION ==
    560      Services.perms.testExactPermissionFromPrincipal(
    561        principal,
    562        "mailto-infobar-dismissed"
    563      )
    564    ) {
    565      let expiry =
    566        Services.perms.getPermissionObject(
    567          principal,
    568          "mailto-infobar-dismissed",
    569          true
    570        ).expireTime - Date.now();
    571 
    572      lazy.log.debug(
    573        `prompt not shown, because it is still dismissed for` +
    574          ` ${principal.host} and will be shown in` +
    575          ` ${(expiry / 1000).toFixed()} seconds again.`
    576      );
    577      return;
    578    }
    579 
    580    // Now show the prompt if there is not already one...
    581    let osDefaultNotificationBox = browser
    582      .getTabBrowser()
    583      .getNotificationBox(browser);
    584 
    585    if (!osDefaultNotificationBox.getNotificationWithValue(notificationId)) {
    586      let win = browser.ownerGlobal;
    587      win.MozXULElement.insertFTLIfNeeded("browser/webProtocolHandler.ftl");
    588 
    589      let notification = await osDefaultNotificationBox.appendNotification(
    590        notificationId,
    591        {
    592          label: {
    593            "l10n-id": "protocolhandler-mailto-handler-set",
    594            "l10n-args": { url: aURI.host },
    595          },
    596          priority: osDefaultNotificationBox.PRIORITY_INFO_LOW,
    597          eventCallback: eventType => {
    598            if (eventType === "dismissed") {
    599              // after a click on 'X' save a timestamp after which we can show
    600              // the prompt again
    601              Services.perms.addFromPrincipal(
    602                principal,
    603                "mailto-infobar-dismissed",
    604                Ci.nsIPermissionManager.DENY_ACTION,
    605                Ci.nsIPermissionManager.EXPIRE_TIME,
    606                lazy.NimbusFeatures.mailto.getVariable(
    607                  "dualPrompt.dismissXClickMinutes"
    608                ) *
    609                  60 *
    610                  1000 +
    611                  Date.now()
    612              );
    613              Glean.protocolhandlerMailto.promptClicked.dismiss_os_default.add();
    614            }
    615          },
    616        },
    617        [
    618          {
    619            "l10n-id": "protocolhandler-mailto-os-handler-yes-button",
    620            primary: true,
    621            callback: newitem => {
    622              let currentHandler = this._addWebProtocolHandler(
    623                aProtocol,
    624                aTitle,
    625                aURI.spec
    626              );
    627              this._setProtocolHandlerDefault(aProtocol, currentHandler);
    628              Glean.protocolhandlerMailto.promptClicked.set_local_default.add();
    629 
    630              if (this._canSetOSDefault(aProtocol)) {
    631                if (this._setOSDefault(aProtocol)) {
    632                  Glean.protocolhandlerMailto.promptClicked.set_os_default.add();
    633                  newitem.messageL10nId =
    634                    "protocolhandler-mailto-handler-confirm";
    635                  newitem.removeChild(newitem.buttonContainer);
    636                  newitem.setAttribute("type", "success"); // from moz-message-bar.css
    637                  newitem.eventCallback = null; // disable show only once per day for success
    638                  return true; // `true` does not hide the bar
    639                }
    640 
    641                // if anything goes wrong with setting the OS default, we want
    642                // to be informed so that we can fix it.
    643                Glean.protocolhandlerMailto.promptClicked.set_os_default_error.add();
    644                return false;
    645              }
    646 
    647              // if the installation does not have an install hash, we cannot
    648              // set the OS default, but mailto links from within the browser
    649              // should still work.
    650              Glean.protocolhandlerMailto.promptClicked.set_os_default_impossible.add();
    651              return false;
    652            },
    653          },
    654          {
    655            "l10n-id": "protocolhandler-mailto-os-handler-no-button",
    656            callback: () => {
    657              // after a click on 'Not Now' save a timestamp after which we can
    658              // show the prompt again
    659              Services.perms.addFromPrincipal(
    660                principal,
    661                "mailto-infobar-dismissed",
    662                Ci.nsIPermissionManager.DENY_ACTION,
    663                Ci.nsIPermissionManager.EXPIRE_TIME,
    664                lazy.NimbusFeatures.mailto.getVariable(
    665                  "dualPrompt.dismissNotNowMinutes"
    666                ) *
    667                  60 *
    668                  1000 +
    669                  Date.now()
    670              );
    671              Glean.protocolhandlerMailto.promptClicked.dismiss_os_default.add();
    672              return false;
    673            },
    674          },
    675        ]
    676      );
    677 
    678      Glean.protocolhandlerMailto.handlerPromptShown.os_default.add();
    679      // remove the icon from the infobar, which is automatically assigned
    680      // after its priority, because the priority is also an indicator which
    681      // type of bar it is, e.g. a warning or error:
    682      notification.setAttribute("type", "system");
    683    }
    684  },
    685 
    686  /**
    687   * See nsISupports
    688   */
    689  QueryInterface: ChromeUtils.generateQI(["nsIWebProtocolHandlerRegistrar"]),
    690 };