/* 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/. */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  CustomizableUI:
    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
  ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
  ExtensionPreferencesManager:
    "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
  IgnoreLists: "resource://gre/modules/IgnoreLists.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

const kPrefName = "browser.startup.homepage";
const kDefaultHomePage = "about:tor";
const kExtensionControllerPref =
  "browser.startup.homepage_override.extensionControlled";
const kHomePageIgnoreListId = "homepage-urls";
const kWidgetId = "home-button";
const kWidgetRemovedPref = "browser.engagement.home-button.has-removed";

function getHomepagePref(useDefault) {
  let homePage;
  let prefs = Services.prefs;
  if (useDefault) {
    prefs = prefs.getDefaultBranch(null);
  }
  try {
    // Historically, this was a localizable pref, but default Firefox builds
    // don't use this.
    // Distributions and local customizations might still use this, so let's
    // keep it.
    homePage = prefs.getComplexValue(kPrefName, Ci.nsIPrefLocalizedString).data;
  } catch (ex) {}

  if (!homePage) {
    homePage = prefs.getStringPref(kPrefName);
  }

  // Apparently at some point users ended up with blank home pages somehow.
  // If that happens, reset the pref and read it again.
  if (!homePage && !useDefault) {
    Services.prefs.clearUserPref(kPrefName);
    homePage = getHomepagePref(true);
  }

  return homePage;
}

/**
 * HomePage provides tools to keep track of the current homepage, and the
 * applications's default homepage. It includes tools to insure that certain
 * urls are ignored. As a result, all set/get requests for the homepage
 * preferences should be routed through here.
 */
export let HomePage = {
  // This is an array of strings that should be matched against URLs to see
  // if they should be ignored or not.
  _ignoreList: [],

  // A promise that is set when initialization starts and resolved when it
  // completes.
  _initializationPromise: null,

  /**
   * Used to initialise the ignore lists. This may be called later than
   * the first call to get or set, which may cause a used to get an ignored
   * homepage, but this is deemed acceptable, as we'll correct it once
   * initialised.
   */
  async delayedStartup() {
    if (this._initializationPromise) {
      await this._initializationPromise;
      return;
    }

    // Now we have the values, listen for future updates.
    this._ignoreListListener = this._handleIgnoreListUpdated.bind(this);

    this._initializationPromise = lazy.IgnoreLists.getAndSubscribe(
      this._ignoreListListener
    );

    this._addCustomizableUiListener();

    const current = await this._initializationPromise;

    await this._handleIgnoreListUpdated({ data: { current } });
  },

  /**
   * Gets the homepage for the given window.
   *
   * @param {DOMWindow} [aWindow]
   *   The window associated with the get, used to check for private browsing
   *   mode. If not supplied, normal mode is assumed.
   * @returns {string}
   *   Returns the home page value, this could be a single url, or a `|`
   *   separated list of URLs.
   */
  get(aWindow) {
    let homePages = getHomepagePref();
    if (
      lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
      (aWindow && lazy.PrivateBrowsingUtils.isWindowPrivate(aWindow))
    ) {
      // If an extension controls the setting and does not have private
      // browsing permission, use the default setting.
      let extensionControlled = Services.prefs.getBoolPref(
        kExtensionControllerPref,
        false
      );
      let privateAllowed = Services.prefs.getBoolPref(
        "browser.startup.homepage_override.privateAllowed",
        false
      );
      // There is a potential on upgrade that the prefs are not set yet, so we double check
      // for moz-extension.
      if (
        !privateAllowed &&
        (extensionControlled || homePages.includes("moz-extension://"))
      ) {
        return this.getDefault();
      }
    }

    if (homePages == "about:blank") {
      homePages = "chrome://browser/content/blanktab.html";
    }

    return homePages;
  },

  getForErrorPage(win) {
    if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
      return win.BROWSER_NEW_TAB_URL;
    }
    let url = this.get(win);
    if (url.includes("|")) {
      url = url.split("|")[0];
    }
    return url;
  },

  /**
   * Convert a pipe-delimited string of custom homepage URLs into an array of
   * trimmed URLs.
   *
   * @param {string} urls
   * @returns {string[]}
   */
  parseCustomHomepageURLs(urls) {
    return urls
      .split("|")
      .map(url => url.trim())
      .filter(Boolean);
  },

  /**
   * Determine if a tab is set to about:preferences or about:settings.
   *
   * @param {Element} tab
   * @returns {boolean}
   */
  isPreferencesOrSettingsTab(tab) {
    return (
      tab.linkedBrowser.currentURI.spec.startsWith("about:preferences") ||
      tab.linkedBrowser.currentURI.spec.startsWith("about:settings")
    );
  },

  /**
   * Get the user's current opened tabs for use as a custom homepage.
   *
   * @param {DOMWindow} [win]
   * @returns {Array}
   */
  getTabsForCustomHomepage(
    win = Services.wm.getMostRecentWindow("navigator:browser")
  ) {
    let tabs = [];

    if (
      win &&
      win.document.documentElement.getAttribute("windowtype") ===
        "navigator:browser"
    ) {
      tabs = win.gBrowser.visibleTabs.slice(win.gBrowser.pinnedTabCount);
      tabs = tabs.filter(tab => !this.isPreferencesOrSettingsTab(tab));

      // XXX: Bug 1441637 - Fix tabbrowser to report tab.closing before it blurs it
      tabs = tabs.filter(tab => !tab.closing);
    }

    return tabs;
  },

  /**
   * @returns {string}
   *   Returns the application default homepage.
   */
  getDefault() {
    return getHomepagePref(true);
  },

  /**
   * @returns {string}
   *   Returns the original application homepage URL (not from prefs).
   */
  getOriginalDefault() {
    return kDefaultHomePage;
  },

  /**
   * @returns {boolean}
   *   Returns true if the homepage has been changed.
   */
  get overridden() {
    return Services.prefs.prefHasUserValue(kPrefName);
  },

  /**
   * @returns {boolean}
   *   Returns true if the homepage preference is locked.
   */
  get locked() {
    return Services.prefs.prefIsLocked(kPrefName);
  },

  /**
   * @returns {boolean}
   *   Returns true if the current homepage is the application default.
   */
  get isDefault() {
    return HomePage.get() === kDefaultHomePage;
  },

  /**
   * Sets the homepage preference to a new page.
   *
   * @param {string} value
   *   The new value to set the preference to. This could be a single url, or a
   *   `|` separated list of URLs.
   */
  async set(value) {
    await this.delayedStartup();

    if (await this.shouldIgnore(value)) {
      console.error(
        `Ignoring homepage setting for ${value} as it is on the ignore list.`
      );
      Glean.homepage.preferenceIgnore.record({ value: "set_blocked" });
      return false;
    }
    Services.prefs.setStringPref(kPrefName, value);
    this._maybeAddHomeButtonToToolbar(value);
    return true;
  },

  /**
   * Sets the homepage preference to a new page. This is an synchronous version
   * that should only be used when we know the source is safe as it bypasses the
   * ignore list, e.g. when setting directly to about:blank or a value not
   * supplied externally.
   *
   * @param {string} value
   *   The new value to set the preference to. This could be a single url, or a
   *   `|` separated list of URLs.
   */
  safeSet(value) {
    Services.prefs.setStringPref(kPrefName, value);
  },

  /**
   * Clears the homepage preference if it is not the default. Note that for
   * policy/locking use, the default homepage might not be about:home after this.
   */
  clear() {
    Services.prefs.clearUserPref(kPrefName);
  },

  /**
   * Resets the homepage preference to be about:home.
   */
  reset() {
    Services.prefs.setStringPref(kPrefName, kDefaultHomePage);
  },

  /**
   * Determines if a url should be ignored according to the ignore list.
   *
   * @param {string} url
   *   A string that is the url or urls to be ignored.
   * @returns {boolean}
   *   True if the url should be ignored.
   */
  async shouldIgnore(url) {
    await this.delayedStartup();

    const lowerURL = url.toLowerCase();
    return this._ignoreList.some(code => lowerURL.includes(code.toLowerCase()));
  },

  /**
   * Handles updates of the ignore list, checking the existing preference and
   * correcting it as necessary.
   *
   * @param {object} eventData
   *   The event data as received from RemoteSettings.
   */
  async _handleIgnoreListUpdated({ data: { current } }) {
    for (const entry of current) {
      if (entry.id == kHomePageIgnoreListId) {
        this._ignoreList = [...entry.matches];
      }
    }

    // Only check if we're overridden as we assume the default value is fine,
    // or won't be changeable (e.g. enterprise policy).
    if (this.overridden) {
      let homePages = getHomepagePref().toLowerCase();
      if (
        this._ignoreList.some(code => homePages.includes(code.toLowerCase()))
      ) {
        if (Services.prefs.getBoolPref(kExtensionControllerPref, false)) {
          if (Services.appinfo.inSafeMode) {
            // Add-ons don't get started in safe mode, so just abort this.
            // We'll get to remove them when we next start in normal mode.
            return;
          }
          // getSetting does not need the module to be loaded.
          const item =
            await lazy.ExtensionPreferencesManager.getSetting(
              "homepage_override"
            );
          if (item && item.id) {
            // During startup some modules may not be loaded yet, so we load
            // the setting we need prior to removal.
            await lazy.ExtensionParent.apiManager.asyncLoadModule(
              "chrome_settings_overrides"
            );
            lazy.ExtensionPreferencesManager.removeSetting(
              item.id,
              "homepage_override"
            ).catch(console.error);
          } else {
            // If we don't have a setting for it, we assume the pref has
            // been incorrectly set somehow.
            Services.prefs.clearUserPref(kExtensionControllerPref);
            Services.prefs.clearUserPref(
              "browser.startup.homepage_override.privateAllowed"
            );
          }
        } else {
          this.clear();
        }
        Glean.homepage.preferenceIgnore.record({ value: "saved_reset" });
      }
    }
  },

  onWidgetRemoved(widgetId) {
    if (widgetId == kWidgetId) {
      Services.prefs.setBoolPref(kWidgetRemovedPref, true);
      lazy.CustomizableUI.removeListener(this);
    }
  },

  /**
   * Add the home button to the toolbar if the user just set a custom homepage.
   *
   * This should only be done once, so we check HOME_BUTTON_REMOVED_PREF which
   * gets set to true when the home button is removed from the toolbar.
   *
   * If the home button is already on the toolbar it won't be moved.
   */
  _maybeAddHomeButtonToToolbar(homePage) {
    if (
      homePage !== "about:home" &&
      homePage !== "about:blank" &&
      !Services.prefs.getBoolPref(kExtensionControllerPref, false) &&
      !Services.prefs.getBoolPref(kWidgetRemovedPref, false) &&
      !lazy.CustomizableUI.getWidget(kWidgetId).areaType
    ) {
      // Find a spot for the home button, ideally it will be in its default
      // position beside the stop/refresh button.
      // Work backwards from the URL bar since it can't be removed and put
      // the button after the first non-spring we find.
      let navbarPlacements = lazy.CustomizableUI.getWidgetIdsInArea("nav-bar");
      let position = navbarPlacements.indexOf("urlbar-container");
      for (let i = position - 1; i >= 0; i--) {
        if (
          !navbarPlacements[i].startsWith("customizableui-special-spring") &&
          !navbarPlacements[i].includes("spacer")
        ) {
          position = i + 1;
          break;
        }
      }
      lazy.CustomizableUI.addWidgetToArea(kWidgetId, "nav-bar", position);
    }
  },

  _addCustomizableUiListener() {
    if (!Services.prefs.getBoolPref(kWidgetRemovedPref, false)) {
      lazy.CustomizableUI.addListener(this);
    }
  },
};
