tor-browser

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

GeckoViewPrompt.sys.mjs (24454B)


      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
      3 * file, 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  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
     11  GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs",
     12  GeckoViewClipboardPermission:
     13    "resource://gre/modules/GeckoViewClipboardPermission.sys.mjs",
     14 });
     15 
     16 const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompt");
     17 
     18 export class PromptFactory {
     19  constructor() {
     20    this.wrappedJSObject = this;
     21  }
     22 
     23  handleEvent(aEvent) {
     24    switch (aEvent.type) {
     25      case "mozshowdropdown":
     26      case "mozshowdropdown-sourcetouch":
     27        this._handleSelect(
     28          aEvent.composedTarget,
     29          aEvent.composedTarget.isCombobox
     30        );
     31        break;
     32      case "MozOpenDateTimePicker":
     33        this._handleDateTime(aEvent.composedTarget);
     34        break;
     35      case "click":
     36        this._handleClick(aEvent);
     37        break;
     38      case "DOMPopupBlocked":
     39        this._handlePopupBlocked(aEvent);
     40        break;
     41      case "DOMRedirectBlocked":
     42        this._handleRedirectBlocked(aEvent);
     43        break;
     44    }
     45  }
     46 
     47  _handleClick(aEvent) {
     48    const target = aEvent.composedTarget;
     49    const className = ChromeUtils.getClassName(target);
     50    if (className !== "HTMLInputElement" && className !== "HTMLSelectElement") {
     51      return;
     52    }
     53 
     54    if (
     55      target.isContentEditable ||
     56      target.disabled ||
     57      target.readOnly ||
     58      !target.willValidate
     59    ) {
     60      // target.willValidate is false when any associated fieldset is disabled,
     61      // in which case this element is treated as disabled per spec.
     62      return;
     63    }
     64 
     65    if (className === "HTMLSelectElement") {
     66      if (!target.isCombobox) {
     67        this._handleSelect(target, /* aIsDropDown = */ false);
     68        return;
     69      }
     70      // combobox select is handled by mozshowdropdown.
     71      return;
     72    }
     73 
     74    const type = target.type;
     75    if (type === "month" || type === "week") {
     76      // If there's a shadow root, the MozOpenDateTimePicker event takes care
     77      // of this. Right now for these input types there's never a shadow root.
     78      // Once we support UA widgets for month/week inputs (see bug 888320), we
     79      // can remove this.
     80      if (!target.openOrClosedShadowRoot) {
     81        this._handleDateTime(target);
     82        aEvent.preventDefault();
     83      }
     84    }
     85  }
     86 
     87  _generateSelectItems(aElement) {
     88    const win = aElement.ownerGlobal;
     89    let id = 0;
     90    const map = {};
     91 
     92    const items = (function enumList(elem, disabled) {
     93      const items = [];
     94      const children = elem.children;
     95      for (let i = 0; i < children.length; i++) {
     96        const child = children[i];
     97        if (win.getComputedStyle(child).display === "none") {
     98          continue;
     99        }
    100        const item = {
    101          id: String(id),
    102          disabled: disabled || child.disabled,
    103        };
    104        if (win.HTMLOptGroupElement.isInstance(child)) {
    105          item.label = child.label;
    106          item.items = enumList(child, item.disabled);
    107        } else if (win.HTMLOptionElement.isInstance(child)) {
    108          item.label = child.label || child.text;
    109          item.selected = child.selected;
    110        } else if (win.HTMLHRElement.isInstance(child)) {
    111          item.separator = true;
    112        } else {
    113          continue;
    114        }
    115        items.push(item);
    116        map[id++] = child;
    117      }
    118      return items;
    119    })(aElement);
    120 
    121    return [items, map, id];
    122  }
    123 
    124  _handleSelect(aElement, aIsDropDown) {
    125    const win = aElement.ownerGlobal;
    126    const [items] = this._generateSelectItems(aElement);
    127 
    128    if (aIsDropDown) {
    129      aElement.openInParentProcess = true;
    130    }
    131 
    132    const prompt = new lazy.GeckoViewPrompter(win);
    133 
    134    // Something changed the <select> while it was open.
    135    const deferredUpdate = new lazy.DeferredTask(() => {
    136      // Inner contents in choice prompt are updated.
    137      const [newItems] = this._generateSelectItems(aElement);
    138      prompt.update({
    139        type: "choice",
    140        mode: aElement.multiple ? "multiple" : "single",
    141        choices: newItems,
    142      });
    143    }, 0);
    144    const mut = new win.MutationObserver(() => {
    145      deferredUpdate.arm();
    146    });
    147    mut.observe(aElement, {
    148      childList: true,
    149      subtree: true,
    150      attributes: true,
    151    });
    152 
    153    const dismissPrompt = () => prompt.dismiss();
    154    aElement.addEventListener("blur", dismissPrompt, { mozSystemGroup: true });
    155    const hidedropdown = event => {
    156      if (aElement === event.target) {
    157        prompt.dismiss();
    158      }
    159    };
    160    const chromeEventHandler = aElement.ownerGlobal.docShell.chromeEventHandler;
    161    chromeEventHandler.addEventListener("mozhidedropdown", hidedropdown, {
    162      mozSystemGroup: true,
    163    });
    164 
    165    prompt.asyncShowPrompt(
    166      {
    167        type: "choice",
    168        mode: aElement.multiple ? "multiple" : "single",
    169        choices: items,
    170      },
    171      result => {
    172        deferredUpdate.disarm();
    173        mut.disconnect();
    174        aElement.removeEventListener("blur", dismissPrompt, {
    175          mozSystemGroup: true,
    176        });
    177        chromeEventHandler.removeEventListener(
    178          "mozhidedropdown",
    179          hidedropdown,
    180          { mozSystemGroup: true }
    181        );
    182 
    183        if (aIsDropDown) {
    184          aElement.openInParentProcess = false;
    185        }
    186        // OK: result
    187        // Cancel: !result
    188        if (!result || result.choices === undefined) {
    189          return;
    190        }
    191 
    192        const [, map, id] = this._generateSelectItems(aElement);
    193        let dispatchEvents = false;
    194        if (!aElement.multiple) {
    195          const elem = map[result.choices[0]];
    196          if (elem && win.HTMLOptionElement.isInstance(elem)) {
    197            dispatchEvents = !elem.selected;
    198            elem.selected = true;
    199          } else {
    200            console.error("Invalid id for select result: " + result.choices[0]);
    201          }
    202        } else {
    203          for (let i = 0; i < id; i++) {
    204            const elem = map[i];
    205            const index = result.choices.indexOf(String(i));
    206            if (
    207              win.HTMLOptionElement.isInstance(elem) &&
    208              elem.selected !== index >= 0
    209            ) {
    210              // Current selected is not the same as the new selected state.
    211              dispatchEvents = true;
    212              elem.selected = !elem.selected;
    213            }
    214            result.choices[index] = undefined;
    215          }
    216          for (let i = 0; i < result.choices.length; i++) {
    217            if (result.choices[i] !== undefined && result.choices[i] !== null) {
    218              console.error(
    219                "Invalid id for select result: " + result.choices[i]
    220              );
    221              break;
    222            }
    223          }
    224        }
    225 
    226        if (dispatchEvents) {
    227          this._dispatchEvents(aElement);
    228        }
    229      }
    230    );
    231  }
    232 
    233  _handleDateTime(aElement) {
    234    const win = aElement.ownerGlobal;
    235    const prompt = new lazy.GeckoViewPrompter(win);
    236 
    237    const chromeEventHandler = aElement.ownerGlobal.docShell.chromeEventHandler;
    238    const dismissPrompt = () => prompt.dismiss();
    239    // Some controls don't have UA widget (bug 888320)
    240    {
    241      const dateTimeBoxElement = aElement.dateTimeBoxElement;
    242      if (["month", "week"].includes(aElement.type) && !dateTimeBoxElement) {
    243        aElement.addEventListener("blur", dismissPrompt, {
    244          mozSystemGroup: true,
    245        });
    246      } else {
    247        chromeEventHandler.addEventListener(
    248          "MozCloseDateTimePicker",
    249          dismissPrompt
    250        );
    251 
    252        dateTimeBoxElement.dispatchEvent(
    253          new win.CustomEvent("MozSetDateTimePickerState", { detail: true })
    254        );
    255      }
    256    }
    257 
    258    prompt.asyncShowPrompt(
    259      {
    260        type: "datetime",
    261        mode: aElement.type,
    262        value: aElement.value,
    263        min: aElement.min,
    264        max: aElement.max,
    265        step: aElement.step,
    266      },
    267      result => {
    268        // Some controls don't have UA widget (bug 888320)
    269        const dateTimeBoxElement = aElement.dateTimeBoxElement;
    270        if (["month", "week"].includes(aElement.type) && !dateTimeBoxElement) {
    271          aElement.removeEventListener("blur", dismissPrompt, {
    272            mozSystemGroup: true,
    273          });
    274        } else {
    275          chromeEventHandler.removeEventListener(
    276            "MozCloseDateTimePicker",
    277            dismissPrompt
    278          );
    279          dateTimeBoxElement.dispatchEvent(
    280            new win.CustomEvent("MozSetDateTimePickerState", { detail: false })
    281          );
    282        }
    283 
    284        // OK: result
    285        // Cancel: !result
    286        if (
    287          !result ||
    288          result.datetime === undefined ||
    289          result.datetime === aElement.value
    290        ) {
    291          return;
    292        }
    293        aElement.value = result.datetime;
    294        this._dispatchEvents(aElement);
    295      }
    296    );
    297  }
    298 
    299  _dispatchEvents(aElement) {
    300    // Fire both "input" and "change" events for <select> and <input> for
    301    // date/time.
    302    aElement.dispatchEvent(
    303      new aElement.ownerGlobal.Event("input", { bubbles: true, composed: true })
    304    );
    305    aElement.dispatchEvent(
    306      new aElement.ownerGlobal.Event("change", { bubbles: true })
    307    );
    308  }
    309 
    310  _handlePopupBlocked(aEvent) {
    311    const dwi = aEvent.requestingWindow;
    312    const popupWindowURISpec = aEvent.popupWindowURI
    313      ? aEvent.popupWindowURI.displaySpec
    314      : "about:blank";
    315 
    316    const prompt = new lazy.GeckoViewPrompter(aEvent.requestingWindow);
    317    prompt.asyncShowPrompt(
    318      {
    319        type: "popup",
    320        targetUri: popupWindowURISpec,
    321      },
    322      ({ response }) => {
    323        if (response && dwi) {
    324          dwi.open(
    325            popupWindowURISpec,
    326            aEvent.popupWindowName,
    327            aEvent.popupWindowFeatures
    328          );
    329        }
    330      }
    331    );
    332  }
    333 
    334  _handleRedirectBlocked(aEvent) {
    335    const dwi = aEvent.requestingWindow;
    336    const redirectURISpec = aEvent.redirectURI.spec;
    337 
    338    const prompt = new lazy.GeckoViewPrompter(aEvent.requestingWindow);
    339    prompt.asyncShowPrompt(
    340      {
    341        type: "redirect",
    342        targetUri: redirectURISpec,
    343      },
    344      ({ response }) => {
    345        if (response && dwi) {
    346          dwi.top.location.href = redirectURISpec;
    347        }
    348      }
    349    );
    350  }
    351 
    352  /* ----------  nsIPromptFactory  ---------- */
    353  getPrompt(aDOMWin, aIID) {
    354    // Delegated to login manager here, which in turn calls back into us via nsIPromptService.
    355    if (aIID.equals(Ci.nsIAuthPrompt2) || aIID.equals(Ci.nsIAuthPrompt)) {
    356      try {
    357        const pwmgr = Cc[
    358          "@mozilla.org/passwordmanager/authpromptfactory;1"
    359        ].getService(Ci.nsIPromptFactory);
    360        return pwmgr.getPrompt(aDOMWin, aIID);
    361      } catch (e) {
    362        console.error("Delegation to password manager failed:", e);
    363      }
    364    }
    365 
    366    const p = new PromptDelegate(aDOMWin);
    367    p.QueryInterface(aIID);
    368    return p;
    369  }
    370 
    371  /* ----------  private memebers  ---------- */
    372 
    373  // nsIPromptService methods proxy to our Prompt class
    374  callProxy(aMethod, aArguments) {
    375    const prompt = new PromptDelegate(aArguments[0]);
    376    let promptArgs;
    377    if (BrowsingContext.isInstance(aArguments[0])) {
    378      // Called by BrowsingContext prompt method, strip modalType.
    379      [, , /*browsingContext*/ /*modalType*/ ...promptArgs] = aArguments;
    380    } else {
    381      [, /*domWindow*/ ...promptArgs] = aArguments;
    382    }
    383    return prompt[aMethod].apply(prompt, promptArgs);
    384  }
    385 
    386  /* ----------  nsIPromptService  ---------- */
    387 
    388  alert() {
    389    return this.callProxy("alert", arguments);
    390  }
    391  alertBC() {
    392    return this.callProxy("alert", arguments);
    393  }
    394  alertCheck() {
    395    return this.callProxy("alertCheck", arguments);
    396  }
    397  alertCheckBC() {
    398    return this.callProxy("alertCheck", arguments);
    399  }
    400  confirm() {
    401    return this.callProxy("confirm", arguments);
    402  }
    403  confirmBC() {
    404    return this.callProxy("confirm", arguments);
    405  }
    406  confirmCheck() {
    407    return this.callProxy("confirmCheck", arguments);
    408  }
    409  confirmCheckBC() {
    410    return this.callProxy("confirmCheck", arguments);
    411  }
    412  confirmEx() {
    413    return this.callProxy("confirmEx", arguments);
    414  }
    415  confirmExBC() {
    416    return this.callProxy("confirmEx", arguments);
    417  }
    418  prompt() {
    419    return this.callProxy("prompt", arguments);
    420  }
    421  promptBC() {
    422    return this.callProxy("prompt", arguments);
    423  }
    424  promptUsernameAndPassword() {
    425    return this.callProxy("promptUsernameAndPassword", arguments);
    426  }
    427  promptUsernameAndPasswordBC() {
    428    return this.callProxy("promptUsernameAndPassword", arguments);
    429  }
    430  promptPassword() {
    431    return this.callProxy("promptPassword", arguments);
    432  }
    433  promptPasswordBC() {
    434    return this.callProxy("promptPassword", arguments);
    435  }
    436  select() {
    437    return this.callProxy("select", arguments);
    438  }
    439  selectBC() {
    440    return this.callProxy("select", arguments);
    441  }
    442  promptAuth() {
    443    return this.callProxy("promptAuth", arguments);
    444  }
    445  promptAuthBC() {
    446    return this.callProxy("promptAuth", arguments);
    447  }
    448  asyncPromptAuth() {
    449    return this.callProxy("asyncPromptAuth", arguments);
    450  }
    451  confirmUserPaste() {
    452    return lazy.GeckoViewClipboardPermission.confirmUserPaste(...arguments);
    453  }
    454 }
    455 
    456 PromptFactory.prototype.classID = Components.ID(
    457  "{076ac188-23c1-4390-aa08-7ef1f78ca5d9}"
    458 );
    459 PromptFactory.prototype.QueryInterface = ChromeUtils.generateQI([
    460  "nsIPromptFactory",
    461  "nsIPromptService",
    462 ]);
    463 
    464 class PromptDelegate {
    465  constructor(aParent) {
    466    this._prompter = new lazy.GeckoViewPrompter(aParent);
    467  }
    468 
    469  BUTTON_TYPE_POSITIVE = 0;
    470  BUTTON_TYPE_NEUTRAL = 1;
    471  BUTTON_TYPE_NEGATIVE = 2;
    472 
    473  /* ---------- internal methods ---------- */
    474 
    475  _addText(aTitle, aText, aMsg) {
    476    return Object.assign(aMsg, {
    477      title: aTitle,
    478      msg: aText,
    479    });
    480  }
    481 
    482  _addCheck(aCheckMsg, aCheckState, aMsg) {
    483    return Object.assign(aMsg, {
    484      hasCheck: !!aCheckMsg,
    485      checkMsg: aCheckMsg,
    486      checkValue: aCheckState && aCheckState.value,
    487    });
    488  }
    489 
    490  /* ----------  nsIPrompt  ---------- */
    491 
    492  alert(aTitle, aText) {
    493    this.alertCheck(aTitle, aText);
    494  }
    495 
    496  alertCheck(aTitle, aText, aCheckMsg, aCheckState) {
    497    const result = this._prompter.showPrompt(
    498      this._addText(
    499        aTitle,
    500        aText,
    501        this._addCheck(aCheckMsg, aCheckState, {
    502          type: "alert",
    503        })
    504      )
    505    );
    506    if (result && aCheckState) {
    507      aCheckState.value = !!result.checkValue;
    508    }
    509  }
    510 
    511  confirm(aTitle, aText) {
    512    // Button 0 is OK.
    513    return this.confirmCheck(aTitle, aText);
    514  }
    515 
    516  confirmCheck(aTitle, aText, aCheckMsg, aCheckState) {
    517    // Button 0 is OK.
    518    return (
    519      this.confirmEx(
    520        aTitle,
    521        aText,
    522        Ci.nsIPrompt.STD_OK_CANCEL_BUTTONS,
    523        /* aButton0 */ null,
    524        /* aButton1 */ null,
    525        /* aButton2 */ null,
    526        aCheckMsg,
    527        aCheckState
    528      ) == 0
    529    );
    530  }
    531 
    532  confirmEx(
    533    aTitle,
    534    aText,
    535    aButtonFlags,
    536    aButton0,
    537    aButton1,
    538    aButton2,
    539    aCheckMsg,
    540    aCheckState
    541  ) {
    542    const btnMap = Array(3).fill(null);
    543    const btnTitle = Array(3).fill(null);
    544    const btnCustomTitle = Array(3).fill(null);
    545    const savedButtonId = [];
    546    for (let i = 0; i < 3; i++) {
    547      const btnFlags = aButtonFlags >> (i * 8);
    548      switch (btnFlags & 0xff) {
    549        case Ci.nsIPrompt.BUTTON_TITLE_OK:
    550          btnMap[this.BUTTON_TYPE_POSITIVE] = i;
    551          btnTitle[this.BUTTON_TYPE_POSITIVE] = "ok";
    552          break;
    553        case Ci.nsIPrompt.BUTTON_TITLE_CANCEL:
    554          btnMap[this.BUTTON_TYPE_NEGATIVE] = i;
    555          btnTitle[this.BUTTON_TYPE_NEGATIVE] = "cancel";
    556          break;
    557        case Ci.nsIPrompt.BUTTON_TITLE_YES:
    558          btnMap[this.BUTTON_TYPE_POSITIVE] = i;
    559          btnTitle[this.BUTTON_TYPE_POSITIVE] = "yes";
    560          break;
    561        case Ci.nsIPrompt.BUTTON_TITLE_NO:
    562          btnMap[this.BUTTON_TYPE_NEGATIVE] = i;
    563          btnTitle[this.BUTTON_TYPE_NEGATIVE] = "no";
    564          break;
    565        case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING:
    566          // We don't know if this is positive/negative/neutral, so save for later.
    567          savedButtonId.push(i);
    568          break;
    569        case Ci.nsIPrompt.BUTTON_TITLE_SAVE:
    570        case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE:
    571        case Ci.nsIPrompt.BUTTON_TITLE_REVERT:
    572        // Not supported; fall-through.
    573        default:
    574          break;
    575      }
    576    }
    577 
    578    // Put saved buttons into available slots.
    579    for (let i = 0; i < 3 && savedButtonId.length; i++) {
    580      if (btnMap[i] === null) {
    581        btnMap[i] = savedButtonId.shift();
    582        btnTitle[i] = "custom";
    583        btnCustomTitle[i] = [aButton0, aButton1, aButton2][btnMap[i]];
    584      }
    585    }
    586 
    587    const result = this._prompter.showPrompt(
    588      this._addText(
    589        aTitle,
    590        aText,
    591        this._addCheck(aCheckMsg, aCheckState, {
    592          type: "button",
    593          btnTitle,
    594          btnCustomTitle,
    595        })
    596      )
    597    );
    598    if (result && aCheckState) {
    599      aCheckState.value = !!result.checkValue;
    600    }
    601    return result && result.button in btnMap ? btnMap[result.button] : -1;
    602  }
    603 
    604  prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) {
    605    const result = this._prompter.showPrompt(
    606      this._addText(
    607        aTitle,
    608        aText,
    609        this._addCheck(aCheckMsg, aCheckState, {
    610          type: "text",
    611          value: aValue.value,
    612        })
    613      )
    614    );
    615    // OK: result && result.text !== undefined
    616    // Cancel: result && result.text === undefined
    617    // Error: !result
    618    if (result && aCheckState) {
    619      aCheckState.value = !!result.checkValue;
    620    }
    621    if (!result || result.text === undefined) {
    622      return false;
    623    }
    624    aValue.value = result.text || "";
    625    return true;
    626  }
    627 
    628  promptPassword(aTitle, aText, aPassword) {
    629    return this._promptUsernameAndPassword(
    630      aTitle,
    631      aText,
    632      /* aUsername */ undefined,
    633      aPassword
    634    );
    635  }
    636 
    637  promptUsernameAndPassword(aTitle, aText, aUsername, aPassword) {
    638    const msg = {
    639      type: "auth",
    640      mode: aUsername ? "auth" : "password",
    641      options: {
    642        flags: aUsername ? 0 : Ci.nsIAuthInformation.ONLY_PASSWORD,
    643        username: aUsername ? aUsername.value : undefined,
    644        password: aPassword.value,
    645      },
    646    };
    647    const result = this._prompter.showPrompt(this._addText(aTitle, aText, msg));
    648    // OK: result && result.password !== undefined
    649    // Cancel: result && result.password === undefined
    650    // Error: !result
    651    if (!result || result.password === undefined) {
    652      return false;
    653    }
    654    if (aUsername) {
    655      aUsername.value = result.username || "";
    656    }
    657    aPassword.value = result.password || "";
    658    return true;
    659  }
    660 
    661  select(aTitle, aText, aSelectList, aOutSelection) {
    662    const choices = Array.prototype.map.call(aSelectList, (item, index) => ({
    663      id: String(index),
    664      label: item,
    665      disabled: false,
    666      selected: false,
    667    }));
    668    const result = this._prompter.showPrompt(
    669      this._addText(aTitle, aText, {
    670        type: "choice",
    671        mode: "single",
    672        choices,
    673      })
    674    );
    675    // OK: result
    676    // Cancel: !result
    677    if (!result || result.choices === undefined) {
    678      return false;
    679    }
    680    aOutSelection.value = Number(result.choices[0]);
    681    return true;
    682  }
    683 
    684  _getAuthMsg(aChannel, aLevel, aAuthInfo) {
    685    let username;
    686    if (
    687      aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN &&
    688      aAuthInfo.domain
    689    ) {
    690      username = aAuthInfo.domain + "\\" + aAuthInfo.username;
    691    } else {
    692      username = aAuthInfo.username;
    693    }
    694    return this._addText(
    695      /* title */ null,
    696      this._getAuthText(aChannel, aAuthInfo),
    697      {
    698        type: "auth",
    699        mode:
    700          aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD
    701            ? "password"
    702            : "auth",
    703        options: {
    704          flags: aAuthInfo.flags,
    705          uri: aChannel && aChannel.URI.displaySpec,
    706          level: aLevel,
    707          username,
    708          password: aAuthInfo.password,
    709        },
    710      }
    711    );
    712  }
    713 
    714  _fillAuthInfo(aAuthInfo, aResult) {
    715    if (!aResult || aResult.password === undefined) {
    716      return false;
    717    }
    718 
    719    aAuthInfo.password = aResult.password || "";
    720    if (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) {
    721      return true;
    722    }
    723 
    724    const username = aResult.username || "";
    725    if (aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
    726      // Domain is separated from username by a backslash
    727      var idx = username.indexOf("\\");
    728      if (idx >= 0) {
    729        aAuthInfo.domain = username.substring(0, idx);
    730        aAuthInfo.username = username.substring(idx + 1);
    731        return true;
    732      }
    733    }
    734    aAuthInfo.username = username;
    735    return true;
    736  }
    737 
    738  promptAuth(aChannel, aLevel, aAuthInfo) {
    739    const result = this._prompter.showPrompt(
    740      this._getAuthMsg(aChannel, aLevel, aAuthInfo)
    741    );
    742    // OK: result && result.password !== undefined
    743    // Cancel: result && result.password === undefined
    744    // Error: !result
    745    return this._fillAuthInfo(aAuthInfo, result);
    746  }
    747 
    748  async asyncPromptAuth(aChannel, aLevel, aAuthInfo) {
    749    const result = await this._prompter.asyncShowPromptPromise(
    750      this._getAuthMsg(aChannel, aLevel, aAuthInfo)
    751    );
    752    // OK: result && result.password !== undefined
    753    // Cancel: result && result.password === undefined
    754    // Error: !result
    755    return this._fillAuthInfo(aAuthInfo, result);
    756  }
    757 
    758  _getAuthText(aChannel, aAuthInfo) {
    759    const isProxy = aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY;
    760    const isPassOnly = aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD;
    761    const isCrossOrig =
    762      aAuthInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE;
    763 
    764    const username = aAuthInfo.username;
    765    const authTarget = this._getAuthTarget(aChannel, aAuthInfo);
    766    const { displayHost } = authTarget;
    767    let { realm } = authTarget;
    768 
    769    // Suppress "the site says: $realm" when we synthesized a missing realm.
    770    if (!aAuthInfo.realm && !isProxy) {
    771      realm = "";
    772    }
    773 
    774    // Trim obnoxiously long realms.
    775    if (realm.length > 50) {
    776      realm = realm.substring(0, 50) + "\u2026";
    777    }
    778 
    779    const bundle = Services.strings.createBundle(
    780      "chrome://global/locale/commonDialogs.properties"
    781    );
    782    let text;
    783    if (isProxy) {
    784      text = bundle.formatStringFromName("EnterLoginForProxy3", [
    785        realm,
    786        displayHost,
    787      ]);
    788    } else if (isPassOnly) {
    789      text = bundle.formatStringFromName("EnterPasswordFor", [
    790        username,
    791        displayHost,
    792      ]);
    793    } else if (isCrossOrig) {
    794      text = bundle.formatStringFromName("EnterUserPasswordForCrossOrigin2", [
    795        displayHost,
    796      ]);
    797    } else if (!realm) {
    798      text = bundle.formatStringFromName("EnterUserPasswordFor2", [
    799        displayHost,
    800      ]);
    801    } else {
    802      text = bundle.formatStringFromName("EnterLoginForRealm3", [
    803        realm,
    804        displayHost,
    805      ]);
    806    }
    807 
    808    return text;
    809  }
    810 
    811  _getAuthTarget(aChannel, aAuthInfo) {
    812    // If our proxy is demanding authentication, don't use the
    813    // channel's actual destination.
    814    if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
    815      if (!(aChannel instanceof Ci.nsIProxiedChannel)) {
    816        throw new Error("proxy auth needs nsIProxiedChannel");
    817      }
    818      const info = aChannel.proxyInfo;
    819      if (!info) {
    820        throw new Error("proxy auth needs nsIProxyInfo");
    821      }
    822      // Proxies don't have a scheme, but we'll use "moz-proxy://"
    823      // so that it's more obvious what the login is for.
    824      const idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
    825        Ci.nsIIDNService
    826      );
    827      const displayHost =
    828        "moz-proxy://" +
    829        idnService.convertUTF8toACE(info.host) +
    830        ":" +
    831        info.port;
    832      let realm = aAuthInfo.realm;
    833      if (!realm) {
    834        realm = displayHost;
    835      }
    836      return { displayHost, realm };
    837    }
    838 
    839    const displayHost =
    840      aChannel.URI.scheme + "://" + aChannel.URI.displayHostPort;
    841    // If a HTTP WWW-Authenticate header specified a realm, that value
    842    // will be available here. If it wasn't set or wasn't HTTP, we'll use
    843    // the formatted hostname instead.
    844    let realm = aAuthInfo.realm;
    845    if (!realm) {
    846      realm = displayHost;
    847    }
    848    return { displayHost, realm };
    849  }
    850 }
    851 
    852 PromptDelegate.prototype.QueryInterface = ChromeUtils.generateQI(["nsIPrompt"]);