Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  AutoCompleteParent.sys.mjs   Sprache: unbekannt

 
/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "DELEGATE_AUTOCOMPLETE",
  "toolkit.autocomplete.delegate",
  false
);

ChromeUtils.defineESModuleGetters(lazy, {
  GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

const PREF_SECURITY_DELAY = "security.notification_enable_delay";

// Stores the actor that has the active popup, used by formfill
let currentActor = null;

let autoCompleteListeners = new Set();

function compareContext(message) {
  if (
    !currentActor ||
    (currentActor.browsingContext != message.data.browsingContext &&
      currentActor.browsingContext.top != message.data.browsingContext)
  ) {
    return false;
  }

  return true;
}

// These are two synchronous messages sent by the child.
// The browsingContext within the message data is either the one that has
// the active autocomplete popup or the top-level of the one that has
// the active autocomplete popup.
Services.ppmm.addMessageListener("AutoComplete:GetSelectedIndex", message => {
  if (compareContext(message)) {
    let actor = currentActor;
    if (actor && actor.openedPopup) {
      return actor.openedPopup.selectedIndex;
    }
  }

  return -1;
});

Services.ppmm.addMessageListener("AutoComplete:SelectBy", message => {
  if (compareContext(message)) {
    let actor = currentActor;
    if (actor && actor.openedPopup) {
      actor.openedPopup.selectBy(message.data.reverse, message.data.page);
    }
  }
});

// AutoCompleteResultView is an abstraction around a list of results.
// It implements enough of nsIAutoCompleteController and
// nsIAutoCompleteInput to make the richlistbox popup work. Since only
// one autocomplete popup should be open at a time, this is a singleton.
var AutoCompleteResultView = {
  // nsISupports
  QueryInterface: ChromeUtils.generateQI([
    "nsIAutoCompleteController",
    "nsIAutoCompleteInput",
  ]),

  // Private variables
  results: [],

  // The AutoCompleteParent currently showing results or null otherwise.
  currentActor: null,

  // nsIAutoCompleteController
  get matchCount() {
    return this.results.length;
  },

  getValueAt(index) {
    return this.results[index].value;
  },

  getFinalCompleteValueAt(index) {
    return this.results[index].value;
  },

  getLabelAt(index) {
    return this.results[index].label;
  },

  getCommentAt(index) {
    return this.results[index].comment;
  },

  getStyleAt(index) {
    return this.results[index].style;
  },

  getImageAt(index) {
    return this.results[index].image;
  },

  handleEnter(aIsPopupSelection) {
    if (this.currentActor) {
      this.currentActor.handleEnter(aIsPopupSelection);
    }
  },

  stopSearch() {},

  searchString: "",

  // nsIAutoCompleteInput
  get controller() {
    return this;
  },

  get popup() {
    return null;
  },

  _focus() {
    if (this.currentActor) {
      this.currentActor.requestFocus();
    }
  },

  // Internal JS-only API
  clearResults() {
    this.currentActor = null;
    this.results = [];
  },

  setResults(actor, results) {
    this.currentActor = actor;
    this.results = results;
  },
};

export class AutoCompleteParent extends JSWindowActorParent {
  didDestroy() {
    if (this.openedPopup) {
      this.openedPopup.closePopup();
    }
  }

  static getCurrentActor() {
    return currentActor;
  }

  static addPopupStateListener(listener) {
    autoCompleteListeners.add(listener);
  }

  static removePopupStateListener(listener) {
    autoCompleteListeners.delete(listener);
  }

  handleEvent(evt) {
    switch (evt.type) {
      case "popupshowing": {
        this.sendAsyncMessage("AutoComplete:PopupOpened", {});
        break;
      }

      case "popuphidden": {
        let selectedIndex = this.openedPopup.selectedIndex;
        let selectedRowComment =
          selectedIndex != -1
            ? AutoCompleteResultView.getCommentAt(selectedIndex)
            : "";
        let selectedRowStyle =
          selectedIndex != -1
            ? AutoCompleteResultView.getStyleAt(selectedIndex)
            : "";

        // Normally preview is cleared after selecting/hovering on a different
        // entry. However, we also need to clear the preview when a pop is closed.
        this.clearAutoCompletePreview();

        this.sendAsyncMessage("AutoComplete:PopupClosed", {
          selectedRowComment,
          selectedRowStyle,
        });
        AutoCompleteResultView.clearResults();
        // adjustHeight clears the height from the popup so that
        // we don't have a big shrink effect if we closed with a
        // large list, and then open on a small one.
        this.openedPopup.adjustHeight();
        this.openedPopup = null;
        currentActor = null;
        evt.target.removeEventListener("popuphidden", this);
        evt.target.removeEventListener("popupshowing", this);
        break;
      }
    }
  }

  showPopupWithResults({ rect, dir, results }) {
    if (!results.length || this.openedPopup) {
      // We shouldn't ever be showing an empty popup, and if we
      // already have a popup open, the old one needs to close before
      // we consider opening a new one.
      return;
    }

    let browser = this.browsingContext.top.embedderElement;
    let window = browser.ownerGlobal;
    // Also check window top in case this is a sidebar.
    if (
      Services.focus.activeWindow !== window.top &&
      Services.focus.focusedWindow.top !== window.top
    ) {
      // We were sent a message from a window or tab that went into the
      // background, so we'll ignore it for now.
      return;
    }

    // Non-empty result styles
    let resultStyles = new Set(results.map(r => r.style).filter(r => !!r));
    currentActor = this;
    this.openedPopup = browser.autoCompletePopup;
    // the layout varies according to different result type
    this.openedPopup.setAttribute("resultstyles", [...resultStyles].join(" "));
    this.openedPopup.hidden = false;
    // don't allow the popup to become overly narrow
    this.openedPopup.style.setProperty(
      "--panel-width",
      Math.max(100, rect.width) + "px"
    );
    this.openedPopup.style.direction = dir;

    AutoCompleteResultView.setResults(this, results);

    this.openedPopup.view = AutoCompleteResultView;
    this.openedPopup.selectedIndex = -1;

    // Reset fields that were set from the last time the search popup was open
    this.openedPopup.mInput = AutoCompleteResultView;
    // Temporarily increase the maxRows as we don't want to show
    // the scrollbar in login or form autofill popups.
    if (
      resultStyles.size &&
      (resultStyles.has("autofill") || resultStyles.has("loginsFooter"))
    ) {
      this.openedPopup._normalMaxRows = this.openedPopup.maxRows;
      this.openedPopup.mInput.maxRows = 10;
    }
    browser.constrainPopup(this.openedPopup);
    this.openedPopup.addEventListener("popuphidden", this);
    this.openedPopup.addEventListener("popupshowing", this);
    this.openedPopup.openPopupAtScreenRect(
      "after_start",
      rect.left,
      rect.top,
      rect.width,
      rect.height,
      false,
      false
    );
    this.openedPopup.invalidate();
    this._maybeRecordTelemetryEvents(results);

    // This is a temporary solution. We should replace it with
    // proper meta information about the popup once such field
    // becomes available.
    let isCreditCard = results.some(result =>
      result?.comment?.includes("cc-number")
    );

    if (isCreditCard) {
      this.delayPopupInput();
    }
  }

  /**
   * @param {object[]} results - Non-empty array of autocomplete results.
   */
  _maybeRecordTelemetryEvents(results) {
    let actor =
      this.browsingContext.currentWindowGlobal.getActor("LoginManager");
    actor.maybeRecordPasswordGenerationShownTelemetryEvent(results);

    // Assume the result with the start time (loginsFooter) is last.
    let lastResult = results[results.length - 1];
    if (lastResult.style != "loginsFooter") {
      return;
    }

    // The comment field of `loginsFooter` results have many additional pieces of
    // information for telemetry purposes. After bug 1555209, this information
    // can be passed to the parent process outside of nsIAutoCompleteResult APIs
    // so we won't need this hack.
    let rawExtraData = JSON.parse(lastResult.comment).telemetryEventData;
    if (!rawExtraData.searchStartTimeMS) {
      throw new Error("Invalid autocomplete search start time");
    }

    if (rawExtraData.stringLength > 1) {
      // To reduce event volume, only record for lengths 0 and 1.
      return;
    }

    let duration =
      Services.telemetry.msSystemNow() - rawExtraData.searchStartTimeMS;
    delete rawExtraData.searchStartTimeMS;

    // Add counts by result style to rawExtraData.
    results.reduce((accumulated, r) => {
      // Ignore learn more as it is only added after importable logins.
      // Do not track generic items in the telemetry.
      if (r.style === "importableLearnMore" || r.style === "generic") {
        return accumulated;
      }

      // Keys can be a maximum of 15 characters and values must be strings.
      // Also treat both "loginWithOrigin" and "login" as "login" as extra_keys
      // is limited to 10.
      let truncatedStyle = r.style.substring(
        0,
        r.style === "loginWithOrigin" ? 5 : 15
      );
      accumulated[truncatedStyle] = (accumulated[truncatedStyle] || 0) + 1;
      return accumulated;
    }, rawExtraData);

    // Even though Glean events do not require converting extra values to
    // strings, keep doing it so booleans keep being encoded as they were for
    // Telemetry recordEvent.
    let extraStrings = Object.fromEntries(
      Object.entries(rawExtraData).map(([key, val]) => {
        let stringVal = "";
        if (typeof val == "boolean") {
          stringVal += val ? "1" : "0";
        } else {
          stringVal += val;
        }
        return [key, stringVal];
      })
    );

    extraStrings.value = duration;
    Glean.formAutocomplete.showLogins.record(extraStrings);
  }

  invalidate(results) {
    if (!this.openedPopup) {
      return;
    }

    if (!results.length) {
      this.closePopup();
    } else {
      AutoCompleteResultView.setResults(this, results);
      this.openedPopup.invalidate();
      this._maybeRecordTelemetryEvents(results);
    }
  }

  closePopup() {
    if (this.openedPopup) {
      // Note that hidePopup() closes the popup immediately,
      // so popuphiding or popuphidden events will be fired
      // and handled during this call.
      this.openedPopup.hidePopup();
    }
  }

  async receiveMessage(message) {
    let browser = this.browsingContext.top.embedderElement;

    if (
      !browser ||
      (!lazy.DELEGATE_AUTOCOMPLETE && !browser.autoCompletePopup)
    ) {
      // If there is no browser or popup, just make sure that the popup has been closed.
      if (this.openedPopup) {
        this.openedPopup.closePopup();
      }

      // Returning false to pacify ESLint, but this return value is
      // ignored by the messaging infrastructure.
      return false;
    }

    switch (message.name) {
      // This is called when an autocomplete entry is selected by the users.
      // In the current design, when a selection is triggered from the parent
      // process (ex, select by mouse click), we still first send the "HandleEnter"
      // message to the child and then child send the "SelectEntry" message back
      // to the parent to indicate that an autocomplete entry is selected.
      case "AutoComplete:SelectEntry": {
        if (this.openedPopup) {
          this.selectAutoCompleteEntry(this.openedPopup.selectedIndex);
        }
        break;
      }

      case "AutoComplete:SetSelectedIndex": {
        let { index } = message.data;
        if (this.openedPopup) {
          this.openedPopup.selectedIndex = index;
        }
        break;
      }

      case "AutoComplete:MaybeOpenPopup": {
        let { results, rect, dir, inputElementIdentifier, formOrigin } =
          message.data;
        if (lazy.DELEGATE_AUTOCOMPLETE) {
          lazy.GeckoViewAutocomplete.delegateSelection({
            browsingContext: this.browsingContext,
            options: results,
            inputElementIdentifier,
            formOrigin,
          });
        } else {
          this.showPopupWithResults({ results, rect, dir });
          this.notifyListeners();

          this.notifyAutoCompletePopupOpened(
            JSON.stringify(inputElementIdentifier)
          );
        }
        break;
      }

      case "AutoComplete:Invalidate": {
        let { results } = message.data;
        this.invalidate(results);
        break;
      }

      case "AutoComplete:ClosePopup": {
        if (lazy.DELEGATE_AUTOCOMPLETE) {
          lazy.GeckoViewAutocomplete.delegateDismiss();
          break;
        }
        this.closePopup();
        break;
      }

      case "AutoComplete:StartSearch": {
        const { searchString, data } = message.data;
        const result = await this.#startSearch(searchString, data);
        return Promise.resolve(result);
      }
    }
    // Returning false to pacify ESLint, but this return value is
    // ignored by the messaging infrastructure.
    return false;
  }

  // Imposes a brief period during which the popup will not respond to
  // a click, so as to reduce the chances of a successful clickjacking
  // attempt
  delayPopupInput() {
    if (!this.openedPopup) {
      return;
    }
    const popupDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);

    // Mochitests set this to 0, and many will fail on integration
    // if we make the popup items inactive, even briefly.
    if (!popupDelay) {
      return;
    }

    const items = Array.from(
      this.openedPopup.getElementsByTagName("richlistitem")
    );
    items.forEach(item => (item.disabled = true));

    lazy.setTimeout(
      () => items.forEach(item => (item.disabled = false)),
      popupDelay
    );
  }

  notifyListeners() {
    let window = this.browsingContext.top.embedderElement.ownerGlobal;
    for (let listener of autoCompleteListeners) {
      try {
        listener(window);
      } catch (ex) {
        console.error(ex);
      }
    }
  }

  /**
   * Despite its name, this handleEnter is only called when the user clicks on
   * one of the items in the popup since the popup is rendered in the parent process.
   * The real controller's handleEnter is called directly in the content process
   * for other methods of completing a selection (e.g. using the tab or enter
   * keys) since the field with focus is in that process.
   * @param {boolean} aIsPopupSelection
   */
  handleEnter(aIsPopupSelection) {
    if (this.openedPopup) {
      this.sendAsyncMessage("AutoComplete:HandleEnter", {
        selectedIndex: this.openedPopup.selectedIndex,
        isPopupSelection: aIsPopupSelection,
      });
    }
  }

  // This defines the supported autocomplete providers and the prioity to show the autocomplete
  // entry.
  #AUTOCOMPLETE_PROVIDERS = ["FormAutofill", "LoginManager", "FormHistory"];

  /**
   * Search across multiple module to gather autocomplete entries for a given search string.
   *
   * @param {string} searchString
   *                 The input string used to query autocomplete entries across different
   *                 autocomplete providers.
   * @param {Array<Object>} providers
   *                        An array of objects where each object has a `name` used to identify the actor
   *                        name of the provider and `options` that are passed to the `searchAutoCompleteEntries`
   *                        method of the actor.
   * @returns {Array<Object>} An array of results objects with `name` of the provider and `entries`
   *          that are returned from the provider module's `searchAutoCompleteEntries` method.
   */
  async #startSearch(searchString, providers) {
    for (const name of this.#AUTOCOMPLETE_PROVIDERS) {
      const provider = providers.find(p => p.actorName == name);
      if (!provider) {
        continue;
      }
      const { actorName, options } = provider;
      const actor =
        this.browsingContext.currentWindowGlobal.getActor(actorName);
      const entries = await actor?.searchAutoCompleteEntries(
        searchString,
        options
      );

      // We have not yet supported showing autocomplete entries from multiple providers,
      if (entries) {
        return [{ actorName, ...entries }];
      }
    }
    return [];
  }

  stopSearch() {}

  // Hard-coded the mapping by using the message prefix to find the actor
  // to process a given message.
  #getActorByMessagePrefix(message) {
    const prefixToActor = [
      { prefix: "PasswordManager", actor: "LoginManager" },
      { prefix: "FormAutofill", actor: "FormAutofill" },
    ];

    const name = prefixToActor.find(x => message.startsWith(x.prefix))?.actor;
    return this.browsingContext.currentWindowGlobal.getActor(name);
  }

  /**
   * When an autocomplete popup is opened, we notify all the autocomplete
   * entry providers that have an entry displayed in this popup.
   *
   * @param {ElementIdentifier} elementId The element with which the autocomplete popup is associated
   */
  notifyAutoCompletePopupOpened(elementId) {
    const actors = new Set();
    for (const result of AutoCompleteResultView.results) {
      try {
        const { fillMessageName } = JSON.parse(result.comment);
        if (!fillMessageName) {
          continue;
        }

        actors.add(this.#getActorByMessagePrefix(fillMessageName));
      } catch {}
    }

    for (const actor of actors) {
      actor.onAutoCompletePopupOpened?.(elementId);
    }
  }

  /**
   * Clear the autocomplete preview
   */
  clearAutoCompletePreview() {
    const selectedIndex = this.openedPopup?.selectedIndex;
    const result = AutoCompleteResultView.results[selectedIndex];
    if (!result) {
      return;
    }

    const { fillMessageName, fillMessageData } = JSON.parse(
      result.comment || "{}"
    );
    if (!fillMessageName) {
      return;
    }

    const actor = this.#getActorByMessagePrefix(fillMessageName);
    actor?.onAutoCompleteEntryClearPreview?.(fillMessageName, fillMessageData);
  }

  /**
   * Show the autocomplete preview for the current selected entry.
   */
  previewAutoCompleteEntry() {
    const selectedIndex = this.openedPopup?.selectedIndex;
    const result = AutoCompleteResultView.results[selectedIndex];
    if (!result) {
      return;
    }

    const { fillMessageName, fillMessageData } = JSON.parse(
      result.comment || "{}"
    );
    if (!fillMessageName) {
      return;
    }

    const actor = this.#getActorByMessagePrefix(fillMessageName);
    actor?.onAutoCompleteEntryHovered?.(fillMessageName, fillMessageData);
  }

  /**
   * When an autocomplete entry is selected, notify the actor that provides the entry
   */
  selectAutoCompleteEntry() {
    const selectedIndex = this.openedPopup?.selectedIndex;
    const result = AutoCompleteResultView.results[selectedIndex];
    if (!result) {
      return;
    }

    const { fillMessageName, fillMessageData } = JSON.parse(
      result.comment || "{}"
    );
    if (!fillMessageName) {
      return;
    }

    const actor = this.#getActorByMessagePrefix(fillMessageName);
    actor?.onAutoCompleteEntrySelected?.(fillMessageName, fillMessageData);
  }

  /**
   * Sends a message to the browser that is requesting the input
   * that the open popup should be focused.
   */
  requestFocus() {
    // Bug 1582722 - See the response in AutoCompleteChild.sys.mjs for why this
    // disabled.
    /*
    if (this.openedPopup) {
      this.sendAsyncMessage("AutoComplete:Focus");
    }
    */
  }
}

[ Dauer der Verarbeitung: 0.37 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge