summaryrefslogtreecommitdiffstats
path: root/comm/chat/modules/InteractiveBrowser.sys.mjs
blob: 700bea8a6170b78ab93a02a2c9d239254f586ac1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/* 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/. */

export class CancelledError extends Error {
  constructor() {
    super("Interactive browser request was cancelled");
  }
}

export var InteractiveBrowser = {
  /**
   * URL to redirect to for completion of the redirect.
   *
   * @type {string}
   */
  COMPLETION_URL: "https://localhost",

  /**
   * Open an interactive browser prompt that should be redirected to the completion URL.
   *
   * @param {string} url - URL to start the interaction from.
   * @param {string} promptText - Prompt for the user for context to the interaction.
   * @returns {Promise<object>} Resolves when the redirect succeeds, else rejects.
   */
  waitForRedirect(url, promptText) {
    return this._browserRequest(url).then(({ window, webProgress, signal }) => {
      window.document.title = promptText;
      return this._listenForRedirect({
        window,
        webProgress,
        signal,
      });
    });
  },

  /**
   * Open a browser window to request an interaction from the user.
   *
   * @param {string} url - URL to load in the browser window
   * @returns {Promise<object>} If the url is loaded, resolves with an object
   * containing the |window|, |webRequest| and a |signal|. The |signal| is an
   * AbortSignal that gets triggered, when the "request is cancelled", i.e. the
   * window is closed.
   */
  _browserRequest(url) {
    return new Promise((resolve, reject) => {
      let browserRequest = {
        promptText: "",
        iconURI: "",
        url,
        _active: true,
        abortController: new AbortController(),
        cancelled() {
          if (!this._active) {
            return;
          }
          reject(new CancelledError());
          this.abortController.abort();
          this._active = false;
        },
        loaded(window, webProgress) {
          if (!this._active) {
            return;
          }
          resolve({ window, webProgress, signal: this.abortController.signal });
        },
      };
      Services.obs.notifyObservers(browserRequest, "browser-request");
    });
  },

  /**
   * Listen for a browser window to redirect to the specified URL.
   *
   * @param {Window} param0.window - Window to listen in.
   * @param {nsIWebProgress} param0.webProgress - Web progress instance.
   * @param {AbortSignal} param0.signal - Abort signal indicating that this should no longer listen for redirects.
   * @returns {Promise<string>} Resolves with the resulting redirect URL.
   */
  _listenForRedirect({ window, webProgress, signal }) {
    return new Promise((resolve, reject) => {
      let listener = {
        QueryInterface: ChromeUtils.generateQI([
          Ci.nsIWebProgressListener,
          Ci.nsISupportsWeakReference,
        ]),
        _abortListener: () => {
          listener._cleanUp();
          reject(new CancelledError());
        },
        _cleanUp() {
          signal.removeEventListener("abort", listener._abortListener);
          webProgress.removeProgressListener(this);
          window.close();
        },
        _checkForRedirect(currentUrl) {
          if (!currentUrl.startsWith(InteractiveBrowser.COMPLETION_URL)) {
            return;
          }
          resolve(currentUrl);

          this._cleanUp();
        },
        onStateChange(aWebProgress, request, stateFlags, aStatus) {
          const wpl = Ci.nsIWebProgressListener;
          if (stateFlags & (wpl.STATE_START | wpl.STATE_IS_NETWORK)) {
            try {
              this._checkForRedirect(request.name);
            } catch (error) {
              // Ignore |name| not implemented exception
              if (error.result !== Cr.NS_ERROR_NOT_IMPLEMENTED) {
                throw error;
              }
            }
          }
        },
        onLocationChange(webProgress, request, location) {
          this._checkForRedirect(location.spec);
        },
        onProgressChange() {},
        onStatusChange() {},
        onSecurityChange() {},
      };

      if (signal.aborted) {
        reject(new CancelledError());
        return;
      }
      signal.addEventListener("abort", listener._abortListener);
      webProgress.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
      const browser = window.document.getElementById("requestFrame");
      if (browser.currentURI.spec) {
        listener._checkForRedirect(browser.currentURI.spec);
      }
    });
  },
};