summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/resources/parent-process-document-event.js
blob: e156a32fe5725ef55ed91ddca6f74d22894f4f4e (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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/* 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/. */

"use strict";

const {
  TYPES: { DOCUMENT_EVENT },
} = require("resource://devtools/server/actors/resources/index.js");
const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
  "devtools.every-frame-target.enabled",
  false
);
const {
  WILL_NAVIGATE_TIME_SHIFT,
} = require("resource://devtools/server/actors/webconsole/listeners/document-events.js");

class ParentProcessDocumentEventWatcher {
  /**
   * Start watching, from the parent process, for DOCUMENT_EVENT's "will-navigate" event related to a given Watcher Actor.
   *
   * All other DOCUMENT_EVENT events are implemented from another watcher class, running in the content process.
   * Note that this other content process watcher will also emit one special edgecase of will-navigate
   * retlated to the iframe dropdown menu.
   *
   * We have to move listen for navigation in the parent to better handle bfcache navigations
   * and more generally all navigations which are initiated from the parent process.
   * 'bfcacheInParent' feature enabled many types of navigations to be controlled from the parent process.
   *
   * This was especially important to have this implementation in the parent
   * because the navigation event may be fired too late in the content process.
   * Leading to will-navigate being emitted *after* the new target we navigate to is notified to the client.
   *
   * @param WatcherActor watcherActor
   *        The watcher actor from which we should observe document event
   * @param Object options
   *        Dictionary object with following attributes:
   *        - onAvailable: mandatory function
   *          This will be called for each resource.
   */
  async watch(watcherActor, { onAvailable }) {
    this.watcherActor = watcherActor;
    this.onAvailable = onAvailable;

    // List of listeners keyed by innerWindowId.
    // Listeners are called as soon as we emitted the will-navigate
    // resource for the related WindowGlobal.
    this._onceWillNavigate = new Map();

    // Filter browsing contexts to only have the top BrowsingContext of each tree of BrowsingContexts…
    const topLevelBrowsingContexts = this.watcherActor
      .getAllBrowsingContexts()
      .filter(browsingContext => browsingContext.top == browsingContext);

    // Only register one WebProgressListener per BrowsingContext tree.
    // We will be notified about children BrowsingContext navigations/state changes via the top level BrowsingContextWebProgressListener,
    // and BrowsingContextWebProgress.browsingContext attribute will be updated dynamically everytime
    // we get notified about a child BrowsingContext.
    // Note that regular web page toolbox will only have one BrowsingContext tree, for the given tab.
    // But the Browser Toolbox will have many trees to listen to, one per top-level Window, and also one per tab,
    // as tabs's BrowsingContext context aren't children of their top level window!
    //
    // Also save the WebProgress and not the BrowsingContext because `BrowsingContext.webProgress` will be undefined in destroy(),
    // while it is still valuable to call `webProgress.removeProgressListener`. Otherwise events keeps being notified!!
    this.webProgresses = topLevelBrowsingContexts.map(
      browsingContext => browsingContext.webProgress
    );
    this.webProgresses.forEach(webProgress => {
      webProgress.addProgressListener(
        this,
        Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
      );
    });
  }

  /**
   * Wait for the emission of will-navigate for a given WindowGlobal
   *
   * @param Number innerWindowId
   *        WindowGlobal's id we want to track
   * @return Promise
   *         Resolves immediatly if the WindowGlobal isn't tracked by any target
   *         -or- resolve later, once the WindowGlobal navigates to another document
   *         and will-navigate has been emitted.
   */
  onceWillNavigateIsEmitted(innerWindowId) {
    // Only delay the target-destroyed event if the target is for BrowsingContext for which we will emit will-navigate
    const isTracked = this.webProgresses.find(
      webProgress =>
        webProgress.browsingContext.currentWindowGlobal.innerWindowId ==
        innerWindowId
    );
    if (isTracked) {
      return new Promise(resolve => {
        this._onceWillNavigate.set(innerWindowId, resolve);
      });
    }
    return Promise.resolve();
  }

  onStateChange(progress, request, flag, status) {
    const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
    const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
    if (isDocument && isStart) {
      const { browsingContext } = progress;
      // Ignore navigation for same-process iframes when EFT is disabled
      if (
        !browsingContext.currentWindowGlobal.isProcessRoot &&
        !isEveryFrameTargetEnabled
      ) {
        return;
      }
      // Ignore if we are still on the initial document,
      // as that's the navigation from it (about:blank) to the actual first location.
      // The target isn't created yet.
      if (browsingContext.currentWindowGlobal.isInitialDocument) {
        return;
      }

      // Only emit will-navigate for top-level targets.
      if (
        this.watcherActor.sessionContext.type == "all" &&
        browsingContext.isContent
      ) {
        // Never emit will-navigate for content browsing contexts in the Browser Toolbox.
        // They might verify `browsingContext.top == browsingContext` because of the chrome/content
        // boundary, but they do not represent a top-level target for this DevTools session.
        return;
      }
      const isTopLevel = browsingContext.top == browsingContext;
      if (!isTopLevel) {
        return;
      }

      const newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
      const { innerWindowId } = browsingContext.currentWindowGlobal;
      this.onAvailable([
        {
          browsingContextID: browsingContext.id,
          innerWindowId,
          resourceType: DOCUMENT_EVENT,
          name: "will-navigate",
          time: Date.now() - WILL_NAVIGATE_TIME_SHIFT,
          isFrameSwitching: false,
          newURI,
        },
      ]);
      const callback = this._onceWillNavigate.get(innerWindowId);
      if (callback) {
        this._onceWillNavigate.delete(innerWindowId);
        callback();
      }
    }
  }

  get QueryInterface() {
    return ChromeUtils.generateQI([
      "nsIWebProgressListener",
      "nsISupportsWeakReference",
    ]);
  }

  destroy() {
    this.webProgresses.forEach(webProgress => {
      webProgress.removeProgressListener(
        this,
        Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
      );
    });
    this.webProgresses = null;
  }
}

module.exports = ParentProcessDocumentEventWatcher;