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"use strict";
6
7var EXPORTED_SYMBOLS = ["DialogHandler"];
8
9var { XPCOMUtils } = ChromeUtils.import(
10  "resource://gre/modules/XPCOMUtils.jsm"
11);
12
13XPCOMUtils.defineLazyModuleGetters(this, {
14  EventEmitter: "resource://gre/modules/EventEmitter.jsm",
15  Services: "resource://gre/modules/Services.jsm",
16});
17
18const DIALOG_TYPES = {
19  ALERT: "alert",
20  BEFOREUNLOAD: "beforeunload",
21  CONFIRM: "confirm",
22  PROMPT: "prompt",
23};
24
25/**
26 * Helper dedicated to detect and interact with browser dialogs such as `alert`,
27 * `confirm` etc. The current implementation only supports tabmodal dialogs,
28 * not full window dialogs.
29 *
30 * Emits "dialog-loaded" when a javascript dialog is opened for the current
31 * browser.
32 *
33 * @param {BrowserElement} browser
34 */
35class DialogHandler {
36  constructor(browser) {
37    EventEmitter.decorate(this);
38    this._dialog = null;
39    this._browser = browser;
40
41    this._onCommonDialogLoaded = this._onCommonDialogLoaded.bind(this);
42    this._onTabDialogLoaded = this._onTabDialogLoaded.bind(this);
43
44    Services.obs.addObserver(
45      this._onCommonDialogLoaded,
46      "common-dialog-loaded"
47    );
48    Services.obs.addObserver(this._onTabDialogLoaded, "tabmodal-dialog-loaded");
49  }
50
51  destructor() {
52    this._dialog = null;
53    this._pageTarget = null;
54
55    Services.obs.removeObserver(
56      this._onCommonDialogLoaded,
57      "common-dialog-loaded"
58    );
59    Services.obs.removeObserver(
60      this._onTabDialogLoaded,
61      "tabmodal-dialog-loaded"
62    );
63  }
64
65  async handleJavaScriptDialog({ accept, promptText }) {
66    if (!this._dialog) {
67      throw new Error("No dialog available for handleJavaScriptDialog");
68    }
69
70    const type = this._getDialogType();
71    if (promptText && type === "prompt") {
72      this._dialog.ui.loginTextbox.value = promptText;
73    }
74
75    const onDialogClosed = new Promise(r => {
76      this._browser.addEventListener("DOMModalDialogClosed", r, {
77        once: true,
78      });
79    });
80
81    // 0 corresponds to the OK callback, 1 to the CANCEL callback.
82    if (accept) {
83      this._dialog.ui.button0.click();
84    } else {
85      this._dialog.ui.button1.click();
86    }
87
88    await onDialogClosed;
89
90    // Resetting dialog to null here might be racy and lead to errors if the
91    // content page is triggering several prompts in a row.
92    // See Bug 1569578.
93    this._dialog = null;
94  }
95
96  _getDialogType() {
97    const { inPermitUnload, promptType } = this._dialog.args;
98
99    if (inPermitUnload) {
100      return DIALOG_TYPES.BEFOREUNLOAD;
101    }
102
103    switch (promptType) {
104      case "alert":
105        return DIALOG_TYPES.ALERT;
106      case "confirm":
107        return DIALOG_TYPES.CONFIRM;
108      case "prompt":
109        return DIALOG_TYPES.PROMPT;
110      default:
111        throw new Error("Unsupported dialog type: " + promptType);
112    }
113  }
114
115  _onCommonDialogLoaded(dialogWindow) {
116    const dialogs = this._browser.tabDialogBox.getContentDialogManager()
117      .dialogs;
118    const dialog = dialogs.find(d => d.frameContentWindow === dialogWindow);
119
120    if (!dialog) {
121      // The dialog is not for the current tab.
122      return;
123    }
124
125    this._dialog = dialogWindow.Dialog;
126    const message = this._dialog.args.text;
127    const type = this._getDialogType();
128
129    this.emit("dialog-loaded", { message, type });
130  }
131
132  _onTabDialogLoaded(promptContainer) {
133    const prompts = this._browser.tabModalPromptBox.listPrompts();
134    const prompt = prompts.find(p => p.ui.promptContainer === promptContainer);
135
136    if (!prompt) {
137      // The dialog is not for the current tab.
138      return;
139    }
140
141    this._dialog = prompt;
142    const message = this._dialog.args.text;
143    const type = this._getDialogType();
144
145    this.emit("dialog-loaded", { message, type });
146  }
147}
148