tor-browser

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

GeckoViewAutocomplete.sys.mjs (20580B)


      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 { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
     11  GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
     12  AddressRecord: "resource://gre/modules/shared/AddressRecord.sys.mjs",
     13  CreditCardRecord: "resource://gre/modules/shared/CreditCardRecord.sys.mjs",
     14 });
     15 
     16 ChromeUtils.defineLazyGetter(lazy, "LoginInfo", () =>
     17  Components.Constructor(
     18    "@mozilla.org/login-manager/loginInfo;1",
     19    "nsILoginInfo",
     20    "init"
     21  )
     22 );
     23 
     24 export class LoginEntry {
     25  constructor({
     26    origin,
     27    formActionOrigin,
     28    httpRealm,
     29    username,
     30    password,
     31    guid,
     32    timeCreated,
     33    timeLastUsed,
     34    timePasswordChanged,
     35    timesUsed,
     36  }) {
     37    this.origin = origin ?? "";
     38    this.formActionOrigin = formActionOrigin ?? null;
     39    this.httpRealm = httpRealm ?? null;
     40    this.username = username ?? "";
     41    this.password = password ?? "";
     42 
     43    // Metadata.
     44    this.guid = guid ?? null;
     45    // TODO: Not supported by GV.
     46    this.timeCreated = timeCreated ?? null;
     47    this.timeLastUsed = timeLastUsed ?? null;
     48    this.timePasswordChanged = timePasswordChanged ?? null;
     49    this.timesUsed = timesUsed ?? null;
     50  }
     51 
     52  toLoginInfo() {
     53    const info = new lazy.LoginInfo(
     54      this.origin,
     55      this.formActionOrigin,
     56      this.httpRealm,
     57      this.username,
     58      this.password
     59    );
     60 
     61    // Metadata.
     62    info.QueryInterface(Ci.nsILoginMetaInfo);
     63    info.guid = this.guid;
     64    info.timeCreated = this.timeCreated;
     65    info.timeLastUsed = this.timeLastUsed;
     66    info.timePasswordChanged = this.timePasswordChanged;
     67    info.timesUsed = this.timesUsed;
     68 
     69    return info;
     70  }
     71 
     72  static parse(aObj) {
     73    const entry = new LoginEntry({});
     74    Object.assign(entry, aObj);
     75 
     76    return entry;
     77  }
     78 
     79  static fromLoginInfo(aInfo) {
     80    const entry = new LoginEntry({});
     81    entry.origin = aInfo.origin;
     82    entry.formActionOrigin = aInfo.formActionOrigin;
     83    entry.httpRealm = aInfo.httpRealm;
     84    entry.username = aInfo.username;
     85    entry.password = aInfo.password;
     86 
     87    // Metadata.
     88    aInfo.QueryInterface(Ci.nsILoginMetaInfo);
     89    entry.guid = aInfo.guid;
     90    entry.timeCreated = aInfo.timeCreated;
     91    entry.timeLastUsed = aInfo.timeLastUsed;
     92    entry.timePasswordChanged = aInfo.timePasswordChanged;
     93    entry.timesUsed = aInfo.timesUsed;
     94 
     95    return entry;
     96  }
     97 }
     98 
     99 export class Address {
    100  constructor({
    101    name,
    102    givenName,
    103    additionalName,
    104    familyName,
    105    organization,
    106    streetAddress,
    107    addressLevel1,
    108    addressLevel2,
    109    addressLevel3,
    110    postalCode,
    111    country,
    112    tel,
    113    email,
    114    guid,
    115    timeCreated,
    116    timeLastUsed,
    117    timeLastModified,
    118    timesUsed,
    119    version,
    120  }) {
    121    this.name = name ?? "";
    122    this.givenName = givenName ?? "";
    123    this.additionalName = additionalName ?? "";
    124    this.familyName = familyName ?? "";
    125    this.organization = organization ?? "";
    126    this.streetAddress = streetAddress ?? "";
    127    this.addressLevel1 = addressLevel1 ?? "";
    128    this.addressLevel2 = addressLevel2 ?? "";
    129    this.addressLevel3 = addressLevel3 ?? "";
    130    this.postalCode = postalCode ?? "";
    131    this.country = country ?? "";
    132    this.tel = tel ?? "";
    133    this.email = email ?? "";
    134 
    135    // Metadata.
    136    this.guid = guid ?? null;
    137    // TODO: Not supported by GV.
    138    this.timeCreated = timeCreated ?? null;
    139    this.timeLastUsed = timeLastUsed ?? null;
    140    this.timeLastModified = timeLastModified ?? null;
    141    this.timesUsed = timesUsed ?? null;
    142    this.version = version ?? null;
    143  }
    144 
    145  isValid() {
    146    return (
    147      (this.name ?? this.givenName ?? this.familyName) !== "" &&
    148      this.streetAddress !== "" &&
    149      this.postalCode !== ""
    150    );
    151  }
    152 
    153  static fromGecko(aObj) {
    154    return new Address({
    155      version: aObj.version,
    156      name: aObj.name,
    157      givenName: aObj["given-name"],
    158      additionalName: aObj["additional-name"],
    159      familyName: aObj["family-name"],
    160      organization: aObj.organization,
    161      streetAddress: aObj["street-address"],
    162      addressLevel1: aObj["address-level1"],
    163      addressLevel2: aObj["address-level2"],
    164      addressLevel3: aObj["address-level3"],
    165      postalCode: aObj["postal-code"],
    166      country: aObj.country,
    167      tel: aObj.tel,
    168      email: aObj.email,
    169      guid: aObj.guid,
    170      timeCreated: aObj.timeCreated,
    171      timeLastUsed: aObj.timeLastUsed,
    172      timeLastModified: aObj.timeLastModified,
    173      timesUsed: aObj.timesUsed,
    174    });
    175  }
    176 
    177  static parse(aObj) {
    178    const entry = new Address({});
    179    Object.assign(entry, aObj);
    180 
    181    return entry;
    182  }
    183 
    184  toGecko() {
    185    let address = {
    186      version: this.version,
    187      name: this.name,
    188      organization: this.organization,
    189      "street-address": this.streetAddress,
    190      "address-level1": this.addressLevel1,
    191      "address-level2": this.addressLevel2,
    192      "address-level3": this.addressLevel3,
    193      "postal-code": this.postalCode,
    194      country: this.country,
    195      tel: this.tel,
    196      email: this.email,
    197      guid: this.guid,
    198      ...(this.givenName && {
    199        "given-name": this.givenName,
    200        "additional-name": this.additionalName,
    201        "family-name": this.familyName,
    202      }),
    203    };
    204 
    205    lazy.AddressRecord.computeFields(address);
    206 
    207    return address;
    208  }
    209 }
    210 
    211 export class CreditCard {
    212  constructor({
    213    name,
    214    number,
    215    expMonth,
    216    expYear,
    217    type,
    218    guid,
    219    timeCreated,
    220    timeLastUsed,
    221    timeLastModified,
    222    timesUsed,
    223    version,
    224  }) {
    225    this.name = name ?? "";
    226    this.number = number ?? "";
    227    this.expMonth = expMonth ?? "";
    228    this.expYear = expYear ?? "";
    229    this.type = type ?? "";
    230 
    231    // Metadata.
    232    this.guid = guid ?? null;
    233    // TODO: Not supported by GV.
    234    this.timeCreated = timeCreated ?? null;
    235    this.timeLastUsed = timeLastUsed ?? null;
    236    this.timeLastModified = timeLastModified ?? null;
    237    this.timesUsed = timesUsed ?? null;
    238    this.version = version ?? null;
    239  }
    240 
    241  isValid() {
    242    return this.number !== "";
    243  }
    244 
    245  static fromGecko(aObj) {
    246    return new CreditCard({
    247      version: aObj.version,
    248      name: aObj["cc-name"],
    249      number: aObj["cc-number"],
    250      expMonth: aObj["cc-exp-month"]?.toString(),
    251      expYear: aObj["cc-exp-year"]?.toString(),
    252      type: aObj["cc-type"],
    253      guid: aObj.guid,
    254      timeCreated: aObj.timeCreated,
    255      timeLastUsed: aObj.timeLastUsed,
    256      timeLastModified: aObj.timeLastModified,
    257      timesUsed: aObj.timesUsed,
    258    });
    259  }
    260 
    261  static parse(aObj) {
    262    const entry = new CreditCard({});
    263    Object.assign(entry, aObj);
    264 
    265    return entry;
    266  }
    267 
    268  toGecko() {
    269    let creditCard = {
    270      version: this.version,
    271      "cc-name": this.name,
    272      "cc-number": this.number,
    273      "cc-exp-month": this.expMonth,
    274      "cc-exp-year": this.expYear,
    275      "cc-type": this.type,
    276      guid: this.guid,
    277    };
    278 
    279    lazy.CreditCardRecord.computeFields(creditCard);
    280 
    281    return creditCard;
    282  }
    283 }
    284 
    285 export class SelectOption {
    286  // Sync with Autocomplete.SelectOption.Hint in Autocomplete.java.
    287  static Hint = {
    288    NONE: 0,
    289    GENERATED: 1 << 0,
    290    INSECURE_FORM: 1 << 1,
    291    DUPLICATE_USERNAME: 1 << 2,
    292    MATCHING_ORIGIN: 1 << 3,
    293  };
    294 
    295  constructor({ value, hint }) {
    296    this.value = value ?? null;
    297    this.hint = hint ?? SelectOption.Hint.NONE;
    298  }
    299 }
    300 
    301 // Sync with Autocomplete.UsedField in Autocomplete.java.
    302 const UsedField = { PASSWORD: 1 };
    303 
    304 export const GeckoViewAutocomplete = {
    305  /** current opened prompt */
    306  _prompt: null,
    307 
    308  /**
    309   * Delegates login entry fetching for the given domain to the attached
    310   * LoginStorage GeckoView delegate.
    311   *
    312   * @param aDomain
    313   *        The domain string to fetch login entries for. If null, all logins
    314   *        will be fetched.
    315   * @return {Promise}
    316   *         Resolves with an array of login objects or null.
    317   *         Rejected if no delegate is attached.
    318   *         Login object string properties:
    319   *         { guid, origin, formActionOrigin, httpRealm, username, password }
    320   */
    321  fetchLogins(aDomain = null) {
    322    debug`fetchLogins for ${aDomain ?? "All domains"}`;
    323 
    324    return lazy.EventDispatcher.instance.sendRequestForResult({
    325      type: "GeckoView:Autocomplete:Fetch:Login",
    326      domain: aDomain,
    327    });
    328  },
    329 
    330  /**
    331   * Delegates credit card entry fetching to the attached LoginStorage
    332   * GeckoView delegate.
    333   *
    334   * @return {Promise}
    335   *         Resolves with an array of credit card objects or null.
    336   *         Rejected if no delegate is attached.
    337   *         Login object string properties:
    338   *         { guid, name, number, expMonth, expYear, type }
    339   */
    340  fetchCreditCards() {
    341    debug`fetchCreditCards`;
    342 
    343    return lazy.EventDispatcher.instance.sendRequestForResult({
    344      type: "GeckoView:Autocomplete:Fetch:CreditCard",
    345    });
    346  },
    347 
    348  /**
    349   * Delegates address entry fetching to the attached LoginStorage
    350   * GeckoView delegate.
    351   *
    352   * @return {Promise}
    353   *         Resolves with an array of address objects or null.
    354   *         Rejected if no delegate is attached.
    355   *         Login object string properties:
    356   *         { guid, name, givenName, additionalName, familyName,
    357   *           organization, streetAddress, addressLevel1, addressLevel2,
    358   *           addressLevel3, postalCode, country, tel, email }
    359   */
    360  fetchAddresses() {
    361    debug`fetchAddresses`;
    362 
    363    return lazy.EventDispatcher.instance.sendRequestForResult({
    364      type: "GeckoView:Autocomplete:Fetch:Address",
    365    });
    366  },
    367 
    368  /**
    369   * Delegates credit card entry saving to the attached LoginStorage GeckoView delegate.
    370   * Call this when a new or modified credit card entry has been submitted.
    371   *
    372   * @param aCreditCard The {CreditCard} to be saved.
    373   */
    374  onCreditCardSave(aCreditCard) {
    375    debug`onCreditCardSave ${aCreditCard}`;
    376 
    377    lazy.EventDispatcher.instance.sendRequest({
    378      type: "GeckoView:Autocomplete:Save:CreditCard",
    379      creditCard: aCreditCard,
    380    });
    381  },
    382 
    383  /**
    384   * Delegates address entry saving to the attached LoginStorage GeckoView delegate.
    385   * Call this when a new or modified address entry has been submitted.
    386   *
    387   * @param aAddress The {Address} to be saved.
    388   */
    389  onAddressSave(aAddress) {
    390    debug`onAddressSave ${aAddress}`;
    391 
    392    lazy.EventDispatcher.instance.sendRequest({
    393      type: "GeckoView:Autocomplete:Save:Address",
    394      address: aAddress,
    395    });
    396  },
    397 
    398  /**
    399   * Delegates login entry saving to the attached LoginStorage GeckoView delegate.
    400   * Call this when a new login entry or a new password for an existing login
    401   * entry has been submitted.
    402   *
    403   * @param aLogin The {LoginEntry} to be saved.
    404   */
    405  onLoginSave(aLogin) {
    406    debug`onLoginSave ${aLogin}`;
    407 
    408    lazy.EventDispatcher.instance.sendRequest({
    409      type: "GeckoView:Autocomplete:Save:Login",
    410      login: aLogin,
    411    });
    412  },
    413 
    414  /**
    415   * Delegates login entry password usage to the attached LoginStorage GeckoView
    416   * delegate.
    417   * Call this when the password of an existing login entry, as returned by
    418   * fetchLogins, has been used for autofill.
    419   *
    420   * @param aLogin The {LoginEntry} whose password was used.
    421   */
    422  onLoginPasswordUsed(aLogin) {
    423    debug`onLoginUsed ${aLogin}`;
    424 
    425    lazy.EventDispatcher.instance.sendRequest({
    426      type: "GeckoView:Autocomplete:Used:Login",
    427      usedFields: UsedField.PASSWORD,
    428      login: aLogin,
    429    });
    430  },
    431 
    432  _numActiveSelections: 0,
    433 
    434  /**
    435   * Delegates login entry selection.
    436   * Call this when there are multiple login entry option for a form to delegate
    437   * the selection.
    438   *
    439   * @param aBrowser The browser instance the triggered the selection.
    440   * @param aOptions The list of {SelectOption} depicting viable options.
    441   */
    442  onLoginSelect(aBrowser, aOptions) {
    443    debug`onLoginSelect ${aOptions}`;
    444 
    445    return new Promise((resolve, reject) => {
    446      if (!aBrowser || !aOptions) {
    447        debug`onLoginSelect Rejecting - no browser or options provided`;
    448        reject();
    449        return;
    450      }
    451 
    452      const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal);
    453      prompt.asyncShowPrompt(
    454        {
    455          type: "Autocomplete:Select:Login",
    456          options: aOptions,
    457        },
    458        result => {
    459          if (!result || !result.selection) {
    460            reject();
    461            return;
    462          }
    463 
    464          const option = new SelectOption({
    465            value: LoginEntry.parse(result.selection.value),
    466            hint: result.selection.hint,
    467          });
    468          resolve(option);
    469        }
    470      );
    471      this._prompt = prompt;
    472    });
    473  },
    474 
    475  /**
    476   * Delegates credit card entry selection.
    477   * Call this when there are multiple credit card entry option for a form to delegate
    478   * the selection.
    479   *
    480   * @param aBrowser The browser instance the triggered the selection.
    481   * @param aOptions The list of {SelectOption} depicting viable options.
    482   */
    483  onCreditCardSelect(aBrowser, aOptions) {
    484    debug`onCreditCardSelect ${aOptions}`;
    485 
    486    return new Promise((resolve, reject) => {
    487      if (!aBrowser || !aOptions) {
    488        debug`onCreditCardSelect Rejecting - no browser or options provided`;
    489        reject();
    490        return;
    491      }
    492 
    493      const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal);
    494      prompt.asyncShowPrompt(
    495        {
    496          type: "Autocomplete:Select:CreditCard",
    497          options: aOptions,
    498        },
    499        result => {
    500          if (!result || !result.selection) {
    501            reject();
    502            return;
    503          }
    504 
    505          const option = new SelectOption({
    506            value: CreditCard.parse(result.selection.value),
    507            hint: result.selection.hint,
    508          });
    509          resolve(option);
    510        }
    511      );
    512      this._prompt = prompt;
    513    });
    514  },
    515 
    516  /**
    517   * Delegates address entry selection.
    518   * Call this when there are multiple address entry option for a form to delegate
    519   * the selection.
    520   *
    521   * @param aBrowser The browser instance the triggered the selection.
    522   * @param aOptions The list of {SelectOption} depicting viable options.
    523   */
    524  onAddressSelect(aBrowser, aOptions) {
    525    debug`onAddressSelect ${aOptions}`;
    526 
    527    return new Promise((resolve, reject) => {
    528      if (!aBrowser || !aOptions) {
    529        debug`onAddressSelect Rejecting - no browser or options provided`;
    530        reject();
    531        return;
    532      }
    533 
    534      const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal);
    535      prompt.asyncShowPrompt(
    536        {
    537          type: "Autocomplete:Select:Address",
    538          options: aOptions,
    539        },
    540        result => {
    541          if (!result || !result.selection) {
    542            reject();
    543            return;
    544          }
    545 
    546          const option = new SelectOption({
    547            value: Address.parse(result.selection.value),
    548            hint: result.selection.hint,
    549          });
    550          resolve(option);
    551        }
    552      );
    553      this._prompt = prompt;
    554    });
    555  },
    556 
    557  async delegateSelection({
    558    browsingContext,
    559    options,
    560    inputElementIdentifier,
    561    formOrigin,
    562  }) {
    563    debug`delegateSelection ${options}`;
    564 
    565    if (!options.length) {
    566      return;
    567    }
    568 
    569    let insecureHint = SelectOption.Hint.NONE;
    570    let loginStyle = null;
    571 
    572    // TODO: Replace this string with more robust mechanics.
    573    let selectionType = null;
    574    const selectOptions = [];
    575 
    576    for (const option of options) {
    577      switch (option.style) {
    578        case "insecureWarning": {
    579          // We depend on the insecure warning to be the first option.
    580          insecureHint = SelectOption.Hint.INSECURE_FORM;
    581          break;
    582        }
    583        case "generatedPassword": {
    584          selectionType = "login";
    585          const comment = JSON.parse(option.comment);
    586          selectOptions.push(
    587            new SelectOption({
    588              value: new LoginEntry({
    589                password: comment.generatedPassword,
    590              }),
    591              hint: SelectOption.Hint.GENERATED | insecureHint,
    592            })
    593          );
    594          break;
    595        }
    596        case "login":
    597        // Fallthrough.
    598        case "loginWithOrigin": {
    599          selectionType = "login";
    600          loginStyle = option.style;
    601          const comment = JSON.parse(option.comment);
    602 
    603          let hint = SelectOption.Hint.NONE | insecureHint;
    604          if (comment.isDuplicateUsername) {
    605            hint |= SelectOption.Hint.DUPLICATE_USERNAME;
    606          }
    607          if (comment.isOriginMatched) {
    608            hint |= SelectOption.Hint.MATCHING_ORIGIN;
    609          }
    610 
    611          selectOptions.push(
    612            new SelectOption({
    613              value: LoginEntry.parse(comment.fillMessageData),
    614              hint,
    615            })
    616          );
    617          break;
    618        }
    619        case "autofill": {
    620          const { fillMessageData } = JSON.parse(option.comment);
    621          const profile = fillMessageData.profile;
    622          debug`delegateSelection - autofill profile ${profile}`;
    623          const creditCard = CreditCard.fromGecko(profile);
    624          const address = Address.fromGecko(profile);
    625          if (creditCard.isValid()) {
    626            selectionType = "creditCard";
    627            selectOptions.push(
    628              new SelectOption({
    629                value: creditCard,
    630                hint: insecureHint,
    631              })
    632            );
    633          } else if (address.isValid()) {
    634            selectionType = "address";
    635            selectOptions.push(
    636              new SelectOption({
    637                value: address,
    638                hint: insecureHint,
    639              })
    640            );
    641          }
    642          break;
    643        }
    644        default:
    645          debug`delegateSelection - ignoring unknown option style ${option.style}`;
    646      }
    647    }
    648 
    649    if (selectOptions.length < 1) {
    650      debug`Abort delegateSelection - no valid options provided`;
    651      return;
    652    }
    653 
    654    if (this._numActiveSelections > 0) {
    655      debug`Abort delegateSelection - there is already one delegation active`;
    656      return;
    657    }
    658 
    659    ++this._numActiveSelections;
    660 
    661    let selectedOption = null;
    662    const browser = browsingContext.top.embedderElement;
    663    if (selectionType === "login") {
    664      selectedOption = await this.onLoginSelect(browser, selectOptions).catch(
    665        _ => {
    666          debug`No GV delegate attached`;
    667        }
    668      );
    669    } else if (selectionType === "creditCard") {
    670      selectedOption = await this.onCreditCardSelect(
    671        browser,
    672        selectOptions
    673      ).catch(_ => {
    674        debug`No GV delegate attached`;
    675      });
    676    } else if (selectionType === "address") {
    677      selectedOption = await this.onAddressSelect(browser, selectOptions).catch(
    678        _ => {
    679          debug`No GV delegate attached`;
    680        }
    681      );
    682    }
    683 
    684    // prompt is closed now.
    685    this._prompt = null;
    686 
    687    --this._numActiveSelections;
    688 
    689    debug`delegateSelection selected option: ${selectedOption}`;
    690 
    691    if (selectionType === "login") {
    692      const selectedLogin = selectedOption?.value?.toLoginInfo();
    693 
    694      if (!selectedLogin) {
    695        debug`Abort delegateSelection - no login entry selected`;
    696        return;
    697      }
    698 
    699      debug`delegateSelection - filling form`;
    700 
    701      if (selectedOption.hint & SelectOption.Hint.GENERATED) {
    702        this.onLoginSave(selectedLogin);
    703      }
    704 
    705      const actor =
    706        browsingContext.currentWindowGlobal.getActor("LoginManager");
    707 
    708      await actor.fillForm({
    709        browser,
    710        inputElementIdentifier,
    711        loginFormOrigin: formOrigin,
    712        login: selectedLogin,
    713        style:
    714          selectedOption.hint & SelectOption.Hint.GENERATED
    715            ? "generatedPassword"
    716            : loginStyle,
    717      });
    718    } else if (selectionType === "creditCard") {
    719      const actor =
    720        browsingContext.currentWindowGlobal.getActor("FormAutofill");
    721      const elementId = JSON.stringify(inputElementIdentifier);
    722      const selectedCreditCard = selectedOption?.value?.toGecko();
    723 
    724      actor.autofillFields(elementId, selectedCreditCard);
    725    } else if (selectionType === "address") {
    726      const actor =
    727        browsingContext.currentWindowGlobal.getActor("FormAutofill");
    728      const elementId = JSON.stringify(inputElementIdentifier);
    729      const selectedAddress = selectedOption?.value?.toGecko();
    730 
    731      actor.autofillFields(elementId, selectedAddress);
    732    }
    733 
    734    debug`delegateSelection - form filled`;
    735  },
    736 
    737  delegateDismiss() {
    738    debug`delegateDismiss`;
    739 
    740    this._prompt?.dismiss();
    741  },
    742 };
    743 
    744 const { debug } = GeckoViewUtils.initLogging("GeckoViewAutocomplete");