/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const {
  saveToDisk,
  alwaysAsk,
  useHelperApp,
  handleInternally,
  useSystemDefault,
} = Ci.nsIHandlerInfo;

const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  kHandlerList: "resource://gre/modules/handlers/HandlerList.sys.mjs",
  kHandlerListVersion: "resource://gre/modules/handlers/HandlerList.sys.mjs",
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
  JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "externalProtocolService",
  "@mozilla.org/uriloader/external-protocol-service;1",
  Ci.nsIExternalProtocolService
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "MIMEService",
  "@mozilla.org/mime;1",
  Ci.nsIMIMEService
);

export function HandlerService() {
  // Observe handlersvc-json-replace so we can switch to the datasource
  Services.obs.addObserver(this, "handlersvc-json-replace", true);
}

HandlerService.prototype = {
  QueryInterface: ChromeUtils.generateQI([
    "nsISupportsWeakReference",
    "nsIHandlerService",
    "nsIObserver",
  ]),

  __store: null,
  get _store() {
    if (!this.__store) {
      this.__store = new lazy.JSONFile({
        path: PathUtils.join(
          Services.dirsvc.get("ProfD", Ci.nsIFile).path,
          "handlers.json"
        ),
        dataPostProcessor: this._dataPostProcessor.bind(this),
      });
    }

    // Always call this even if this.__store was set, since it may have been
    // set by asyncInit, which might not have completed yet.
    this._ensureStoreInitialized();
    return this.__store;
  },

  __storeInitialized: false,
  _ensureStoreInitialized() {
    if (!this.__storeInitialized) {
      this.__storeInitialized = true;
      this.__store.ensureDataReady();

      this._injectDefaultProtocolHandlersIfNeeded();
      this._migrateProtocolHandlersIfNeeded();

      Services.obs.notifyObservers(null, "handlersvc-store-initialized");
    }
  },

  _dataPostProcessor(data) {
    return data.defaultHandlersVersion
      ? data
      : {
          defaultHandlersVersion: {},
          mimeTypes: {},
          schemes: {},
          isDownloadsImprovementsAlreadyMigrated: false,
        };
  },

  /**
   * Injects new default protocol handlers if the version in the preferences is
   * newer than the one in the data store.
   */
  _injectDefaultProtocolHandlersIfNeeded() {
    try {
      let defaultHandlersVersion = Services.prefs.getIntPref(
        "gecko.handlerService.defaultHandlersVersion",
        0
      );
      if (defaultHandlersVersion < lazy.kHandlerListVersion) {
        this._injectDefaultProtocolHandlers();
        Services.prefs.setIntPref(
          "gecko.handlerService.defaultHandlersVersion",
          lazy.kHandlerListVersion
        );
        // Now save the result:
        this._store.saveSoon();
      }
    } catch (ex) {
      console.error(ex);
    }
  },

  _injectDefaultProtocolHandlers() {
    let locale = Services.locale.appLocaleAsBCP47;

    // Initialize handlers to default and update based on locale.
    let localeHandlers = lazy.kHandlerList.default;
    if (lazy.kHandlerList[locale]) {
      for (let scheme in lazy.kHandlerList[locale].schemes) {
        localeHandlers.schemes[scheme] =
          lazy.kHandlerList[locale].schemes[scheme];
      }
    }

    // Now, we're going to cheat. Terribly. The idiologically correct way
    // of implementing the following bit of code would be to fetch the
    // handler info objects from the protocol service, manipulate those,
    // and then store each of them.
    // However, that's expensive. It causes us to talk to the OS about
    // default apps, which causes the OS to go hit the disk.
    // All we're trying to do is insert some web apps into the list. We
    // don't care what's already in the file, we just want to do the
    // equivalent of appending into the database. So let's just go do that:
    for (let scheme of Object.keys(localeHandlers.schemes)) {
      if (scheme == "mailto" && AppConstants.MOZ_APP_NAME == "thunderbird") {
        // Thunderbird IS a mailto handler, it doesn't need handlers added.
        continue;
      }

      let existingSchemeInfo = this._store.data.schemes[scheme];
      if (!existingSchemeInfo) {
        // Haven't seen this scheme before. Default to asking which app the
        // user wants to use:
        existingSchemeInfo = {
          // Signal to future readers that we didn't ask the OS anything.
          // When the entry is first used, get the info from the OS.
          stubEntry: true,
          // The first item in the list is the preferred handler, and
          // there isn't one, so we fill in null:
          handlers: [null],
        };
        this._store.data.schemes[scheme] = existingSchemeInfo;
      }
      let { handlers } = existingSchemeInfo;
      for (let newHandler of localeHandlers.schemes[scheme].handlers) {
        if (!newHandler.uriTemplate) {
          console.error(
            `Ignoring protocol handler for ${scheme} without a uriTemplate!`
          );
          continue;
        }
        if (!newHandler.uriTemplate.startsWith("https://")) {
          console.error(
            `Ignoring protocol handler for ${scheme} with insecure template URL ${newHandler.uriTemplate}.`
          );
          continue;
        }
        if (!newHandler.uriTemplate.toLowerCase().includes("%s")) {
          console.error(
            `Ignoring protocol handler for ${scheme} with invalid template URL ${newHandler.uriTemplate}.`
          );
          continue;
        }
        // If there is already a handler registered with the same template
        // URL, ignore the new one:
        let matchingTemplate = handler =>
          handler && handler.uriTemplate == newHandler.uriTemplate;
        if (!handlers.some(matchingTemplate)) {
          handlers.push(newHandler);
        }
      }
    }
  },

  /**
   * Execute any migrations. Migrations are defined here for any changes or removals for
   * existing handlers. Additions are still handled via the localized prefs infrastructure.
   *
   * This depends on the browser.handlers.migrations pref being set by migrateUI in
   * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products.
   * This is a comma-separated list of identifiers of migrations that need running.
   * This avoids both re-running older migrations and keeping an additional
   * pref around permanently.
   */
  _migrateProtocolHandlersIfNeeded() {
    function removeMatchingHandlers(info, regex) {
      let matches = app =>
        app instanceof Ci.nsIWebHandlerApp && regex.test(app.uriTemplate);
      let shouldStore = false;
      // First remove the app from possible handlers.
      let handlers = info.possibleApplicationHandlers;
      for (let i = handlers.length - 1; i >= 0; i--) {
        let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
        if (matches(app)) {
          shouldStore = true;
          handlers.removeElementAt(i);
        }
      }
      // Then remove as a preferred handler.
      if (info.preferredApplicationHandler) {
        let app = info.preferredApplicationHandler;
        if (matches(app)) {
          info.preferredApplicationHandler = null;
          shouldStore = true;
        }
      }
      return shouldStore;
    }
    const kMigrations = {
      "30boxes": () => {
        const k30BoxesRegex =
          /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
        let webcalHandler =
          lazy.externalProtocolService.getProtocolHandlerInfo("webcal");
        if (this.exists(webcalHandler)) {
          this.fillHandlerInfo(webcalHandler, "");
          // Remove any matching handlers and store the result if we did.
          if (removeMatchingHandlers(webcalHandler, k30BoxesRegex)) {
            this.store(webcalHandler);
          }
        }
      },
      // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
      "secure-mail": () => {
        const kSubstitutions = new Map([
          [
            "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
            "https://mail.yahoo.co.jp/compose/?To=%s",
          ],
          [
            "http://www.inbox.lv/rfc2368/?value=%s",
            "https://mail.inbox.lv/compose?to=%s",
          ],
          [
            "http://poczta.interia.pl/mh/?mailto=%s",
            "https://poczta.interia.pl/mh/?mailto=%s",
          ],
          [
            "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
            "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
          ],
        ]);

        function maybeReplaceURL(app) {
          if (app instanceof Ci.nsIWebHandlerApp) {
            let { uriTemplate } = app;
            let sub = kSubstitutions.get(uriTemplate);
            if (sub) {
              app.uriTemplate = sub;
              return true;
            }
          }
          return false;
        }
        let mailHandler =
          lazy.externalProtocolService.getProtocolHandlerInfo("mailto");
        if (this.exists(mailHandler)) {
          this.fillHandlerInfo(mailHandler, "");
          let handlers = mailHandler.possibleApplicationHandlers;
          let shouldStore = false;
          for (let i = handlers.length - 1; i >= 0; i--) {
            let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
            // Note: will evaluate the RHS because it's a binary rather than
            // logical or.
            shouldStore |= maybeReplaceURL(app);
          }
          // Then check the preferred handler.
          if (mailHandler.preferredApplicationHandler) {
            shouldStore |= maybeReplaceURL(
              mailHandler.preferredApplicationHandler
            );
          }
          // Then store, if we changed anything. Note that store() handles
          // duplicates, so we don't have to.
          if (shouldStore) {
            this.store(mailHandler);
          }
        }
      },
      mibbit: () => {
        const mibbitRegex = /^https?:\/\/(?:www\.)?mibbit\.com/i;
        for (let prot of ["irc", "ircs"]) {
          let protInfo =
            lazy.externalProtocolService.getProtocolHandlerInfo(prot);
          if (this.exists(protInfo)) {
            this.fillHandlerInfo(protInfo, "");
            // Remove any matching handlers and store the result if we did.
            if (removeMatchingHandlers(protInfo, mibbitRegex)) {
              this.store(protInfo);
            }
          }
        }
      },
    };
    let migrationsToRun = Services.prefs.getCharPref(
      "browser.handlers.migrations",
      ""
    );
    migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
    for (let migration of migrationsToRun) {
      migration.trim();
      try {
        kMigrations[migration]();
      } catch (ex) {
        console.error(ex);
      }
    }

    if (migrationsToRun.length) {
      Services.prefs.clearUserPref("browser.handlers.migrations");
    }
  },

  _onDBChange() {
    return (async () => {
      if (this.__store) {
        await this.__store.finalize();
      }
      this.__store = null;
      this.__storeInitialized = false;
    })().catch(console.error);
  },

  // nsIObserver
  observe(subject, topic) {
    if (topic != "handlersvc-json-replace") {
      return;
    }
    let promise = this._onDBChange();
    promise.then(() => {
      Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
    });
  },

  // nsIHandlerService
  asyncInit() {
    if (!this.__store) {
      this.__store = new lazy.JSONFile({
        path: PathUtils.join(
          Services.dirsvc.get("ProfD", Ci.nsIFile).path,
          "handlers.json"
        ),
        dataPostProcessor: this._dataPostProcessor.bind(this),
      });
      this.__store
        .load()
        .then(() => {
          // __store can be null if we called _onDBChange in the mean time.
          if (this.__store) {
            this._ensureStoreInitialized();
          }
        })
        .catch(console.error);
    }
  },

  // nsIHandlerService
  enumerate() {
    let handlers = Cc["@mozilla.org/array;1"].createInstance(
      Ci.nsIMutableArray
    );
    for (let [type, typeInfo] of Object.entries(this._store.data.mimeTypes)) {
      let primaryExtension = typeInfo.extensions?.[0] ?? null;
      let handler = lazy.MIMEService.getFromTypeAndExtension(
        type,
        primaryExtension
      );
      handlers.appendElement(handler);
    }
    for (let type of Object.keys(this._store.data.schemes)) {
      // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive
      // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo
      // until one of its properties is accessed.
      //
      // Note: our caller still needs to yield periodically when iterating
      // the enumerator and accessing handler properties to avoid monopolizing
      // the main thread.
      //
      let handler = new Proxy(
        {
          QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
          type,
          get _handlerInfo() {
            delete this._handlerInfo;
            return (this._handlerInfo =
              lazy.externalProtocolService.getProtocolHandlerInfo(type));
          },
        },
        {
          get(target, name) {
            return target[name] || target._handlerInfo[name];
          },
          set(target, name, value) {
            target._handlerInfo[name] = value;
          },
        }
      );
      handlers.appendElement(handler);
    }
    return handlers.enumerate(Ci.nsIHandlerInfo);
  },

  // nsIHandlerService
  store(handlerInfo) {
    let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo);

    // Retrieve an existing entry if present, instead of creating a new one, so
    // that we preserve unknown properties for forward compatibility.
    let storedHandlerInfo = handlerList[handlerInfo.type];
    if (!storedHandlerInfo) {
      storedHandlerInfo = {};
      handlerList[handlerInfo.type] = storedHandlerInfo;
    }

    // Only a limited number of preferredAction values is allowed.
    if (
      handlerInfo.preferredAction == saveToDisk ||
      handlerInfo.preferredAction == useSystemDefault ||
      handlerInfo.preferredAction == handleInternally ||
      // For files (ie mimetype rather than protocol handling info), ensure
      // we can store the "always ask" state, too:
      (handlerInfo.preferredAction == alwaysAsk &&
        this._isMIMEInfo(handlerInfo))
    ) {
      storedHandlerInfo.action = handlerInfo.preferredAction;
    } else {
      storedHandlerInfo.action = useHelperApp;
    }

    if (handlerInfo.alwaysAskBeforeHandling) {
      storedHandlerInfo.ask = true;
    } else {
      delete storedHandlerInfo.ask;
    }

    // Build a list of unique nsIHandlerInfo instances to process later.
    let handlers = [];
    if (handlerInfo.preferredApplicationHandler) {
      handlers.push(handlerInfo.preferredApplicationHandler);
    }
    for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
      Ci.nsIHandlerApp
    )) {
      // If the caller stored duplicate handlers, we save them only once.
      if (!handlers.some(h => h.equals(handler))) {
        handlers.push(handler);
      }
    }

    // If any of the nsIHandlerInfo instances cannot be serialized, it is not
    // included in the final list. The first element is always the preferred
    // handler, or null if there is none.
    let serializableHandlers = handlers
      .map(h => this.handlerAppToSerializable(h))
      .filter(h => h);
    if (serializableHandlers.length) {
      if (!handlerInfo.preferredApplicationHandler) {
        serializableHandlers.unshift(null);
      }
      storedHandlerInfo.handlers = serializableHandlers;
    } else {
      delete storedHandlerInfo.handlers;
    }

    if (this._isMIMEInfo(handlerInfo)) {
      let extensions = storedHandlerInfo.extensions || [];
      for (let extension of handlerInfo.getFileExtensions()) {
        extension = extension.toLowerCase();
        // If the caller stored duplicate extensions, we save them only once.
        if (!extensions.includes(extension)) {
          extensions.push(extension);
        }
      }
      if (extensions.length) {
        storedHandlerInfo.extensions = extensions;
      } else {
        delete storedHandlerInfo.extensions;
      }
    }

    // If we're saving *anything*, it stops being a stub:
    delete storedHandlerInfo.stubEntry;

    this._store.saveSoon();

    // Now notify PDF.js. This is hacky, but a lot better than expecting all
    // the consumers to do it...
    if (handlerInfo.type == "application/pdf") {
      Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED);
    }

    if (handlerInfo.type == "mailto") {
      Services.obs.notifyObservers(null, "mailto::onClearCache");
    }
  },

  // nsIHandlerService
  fillHandlerInfo(handlerInfo, overrideType) {
    let type = overrideType || handlerInfo.type;
    let storedHandlerInfo =
      this._getHandlerListByHandlerInfoType(handlerInfo)[type];
    if (!storedHandlerInfo) {
      throw new Components.Exception(
        "handlerSvc fillHandlerInfo: don't know this type",
        Cr.NS_ERROR_NOT_AVAILABLE
      );
    }

    let isStub = !!storedHandlerInfo.stubEntry;
    // In the normal case, this is not a stub, so we can just read stored info
    // and write to the handlerInfo object we were passed.
    if (!isStub) {
      handlerInfo.preferredAction = storedHandlerInfo.action;
      handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
    } else {
      // If we've got a stub, ensure the defaults are still set:
      lazy.externalProtocolService.setProtocolHandlerDefaults(
        handlerInfo,
        handlerInfo.hasDefaultHandler
      );
      if (
        handlerInfo.preferredAction == alwaysAsk &&
        handlerInfo.alwaysAskBeforeHandling
      ) {
        // `store` will default to `useHelperApp` because `alwaysAsk` is
        // not one of the 3 recognized options; for compatibility, do
        // the same here.
        handlerInfo.preferredAction = useHelperApp;
      }
    }
    // If it *is* a stub, don't override alwaysAskBeforeHandling or the
    // preferred actions. Instead, just append the stored handlers, without
    // overriding the preferred app, and then schedule a task to store proper
    // info for this handler.
    this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub);

    if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) {
      for (let extension of storedHandlerInfo.extensions) {
        handlerInfo.appendExtension(extension);
      }
    } else if (this._mockedHandler) {
      this._insertMockedHandler(handlerInfo);
    }
  },

  /**
   * Private method to inject stored handler information into an nsIHandlerInfo
   * instance.
   *
   * @param handlerInfo           the nsIHandlerInfo instance to write to
   * @param storedHandlers        the stored handlers
   * @param keepPreferredApp      whether to keep the handlerInfo's
   *                              preferredApplicationHandler or override it
   *                              (default: false, ie override it)
   */
  _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) {
    // If the first item is not null, it is also the preferred handler. Since
    // we cannot modify the stored array, use a boolean to keep track of this.
    let isFirstItem = true;
    for (let handler of storedHandlers || [null]) {
      let handlerApp = this.handlerAppFromSerializable(handler || {});
      if (isFirstItem) {
        isFirstItem = false;
        // Do not overwrite the preferred app unless that's allowed
        if (!keepPreferredApp) {
          handlerInfo.preferredApplicationHandler = handlerApp;
        }
      }
      if (handlerApp) {
        handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
      }
    }
  },

  /**
   * @param handler
   *        A nsIHandlerApp handler app
   * @returns  Serializable representation of a handler app object.
   */
  handlerAppToSerializable(handler) {
    if (handler instanceof Ci.nsILocalHandlerApp) {
      return {
        name: handler.name,
        path: handler.executable.path,
      };
    } else if (handler instanceof Ci.nsIWebHandlerApp) {
      return {
        name: handler.name,
        uriTemplate: handler.uriTemplate,
      };
    } else if (handler instanceof Ci.nsIGIOMimeApp) {
      return {
        name: handler.name,
        command: handler.command,
      };
    } else if (handler instanceof Ci.nsIGIOHandlerApp) {
      return {
        name: handler.name,
        id: handler.id,
      };
    }
    // If the handler is an unknown handler type, return null.
    // Android default application handler is the case.
    return null;
  },

  /**
   * @param handlerObj
   *        Serializable representation of a handler object.
   * @returns  {nsIHandlerApp}  the handler app, if any; otherwise null
   */
  handlerAppFromSerializable(handlerObj) {
    let handlerApp;
    if ("path" in handlerObj) {
      try {
        let file = new lazy.FileUtils.File(handlerObj.path);
        if (!file.exists()) {
          return null;
        }
        handlerApp = Cc[
          "@mozilla.org/uriloader/local-handler-app;1"
        ].createInstance(Ci.nsILocalHandlerApp);
        handlerApp.executable = file;
      } catch (ex) {
        return null;
      }
    } else if ("uriTemplate" in handlerObj) {
      handlerApp = Cc[
        "@mozilla.org/uriloader/web-handler-app;1"
      ].createInstance(Ci.nsIWebHandlerApp);
      handlerApp.uriTemplate = handlerObj.uriTemplate;
    } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
      try {
        handlerApp = Cc["@mozilla.org/gio-service;1"]
          .getService(Ci.nsIGIOService)
          .createAppFromCommand(handlerObj.command, handlerObj.name);
      } catch (ex) {
        return null;
      }
    } else if ("id" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
      try {
        handlerApp = Cc["@mozilla.org/gio-service;1"]
          .getService(Ci.nsIGIOService)
          .createHandlerAppFromAppId(handlerObj.id);
      } catch (ex) {
        return null;
      }
    } else {
      return null;
    }

    handlerApp.name = handlerObj.name;
    return handlerApp;
  },

  /**
   * The function returns a reference to the "mimeTypes" or "schemes" object
   * based on which type of handlerInfo is provided.
   */
  _getHandlerListByHandlerInfoType(handlerInfo) {
    return this._isMIMEInfo(handlerInfo)
      ? this._store.data.mimeTypes
      : this._store.data.schemes;
  },

  /**
   * Determines whether an nsIHandlerInfo instance represents a MIME type.
   */
  _isMIMEInfo(handlerInfo) {
    // We cannot rely only on the instanceof check because on Android both MIME
    // types and protocols are instances of nsIMIMEInfo. We still do the check
    // so that properties of nsIMIMEInfo become available to the callers.
    return (
      handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
    );
  },

  // nsIHandlerService
  exists(handlerInfo) {
    return (
      handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
    );
  },

  // nsIHandlerService
  remove(handlerInfo) {
    delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
    this._store.saveSoon();
  },

  // nsIHandlerService
  getTypeFromExtension(fileExtension) {
    let extension = fileExtension.toLowerCase();
    let mimeTypes = this._store.data.mimeTypes;
    for (let type of Object.keys(mimeTypes)) {
      if (
        mimeTypes[type].extensions &&
        mimeTypes[type].extensions.includes(extension)
      ) {
        return type;
      }
    }
    return "";
  },

  _mockedHandler: null,
  _mockedProtocol: null,

  _insertMockedHandler(handlerInfo) {
    if (handlerInfo.type == this._mockedProtocol) {
      handlerInfo.preferredApplicationHandler = this._mockedHandler;
      handlerInfo.possibleApplicationHandlers.insertElementAt(
        this._mockedHandler,
        0
      );
    }
  },

  // test-only: mock the handler instance for a particular protocol/scheme
  mockProtocolHandler(protocol) {
    if (!protocol) {
      this._mockedProtocol = null;
      this._mockedHandler = null;
      return;
    }
    this._mockedProtocol = protocol;
    this._mockedHandler = {
      QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]),
      launchWithURI(uri) {
        Services.obs.notifyObservers(uri, "mocked-protocol-handler");
      },
      name: "Mocked handler",
      detailedDescription: "Mocked handler for tests",
      equals(x) {
        return x == this;
      },
      get executable() {
        if (AppConstants.platform == "macosx") {
          // We need an app path that isn't us, nor in our app bundle, and
          // Apple no longer allows us to read the default-shipped apps
          // in /Applications/ - except for Safari, it would appear!
          let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
          f.initWithPath("/Applications/Safari.app");
          return f;
        }
        return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
      },
      parameterCount: 0,
      clearParameters() {},
      appendParameter() {},
      getParameter() {},
      parameterExists() {
        return false;
      },
    };
  },
};
