summaryrefslogtreecommitdiffstats
path: root/remote/webdriver-bidi/modules/root/session.sys.mjs
blob: 8ecf7d77241bac2c4ab2c114a0a0abb75d31c220 (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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
/* 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/. */

import { Module } from "chrome://remote/content/shared/messagehandler/Module.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
  ContextDescriptorType:
    "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
  Marionette: "chrome://remote/content/components/Marionette.sys.mjs",
  RootMessageHandler:
    "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
  TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
});

class SessionModule extends Module {
  #browsingContextIdEventMap;
  #globalEventSet;

  constructor(messageHandler) {
    super(messageHandler);

    // Map with top-level browsing context id keys and values
    // that are a set of event names for events
    // that are enabled in the given browsing context.
    // TODO: Bug 1804417. Use navigable instead of browsing context id.
    this.#browsingContextIdEventMap = new Map();

    // Set of event names which are strings of the form [moduleName].[eventName]
    // for events that are enabled for all browsing contexts.
    // We should only add an actual event listener on the MessageHandler the
    // first time an event is subscribed to.
    this.#globalEventSet = new Set();
  }

  destroy() {
    this.#browsingContextIdEventMap = null;
    this.#globalEventSet = null;
  }

  /**
   * Commands
   */

  /**
   * End the current session.
   *
   * Session clean up will happen later in WebDriverBiDiConnection class.
   */
  async end() {
    if (lazy.Marionette.running) {
      throw new lazy.error.UnsupportedOperationError(
        "Ending session which was started with Webdriver classic is not supported, use Webdriver classic delete command instead."
      );
    }
  }

  /**
   * Enable certain events either globally, or for a list of browsing contexts.
   *
   * @param {object=} params
   * @param {Array<string>} params.events
   *     List of events to subscribe to.
   * @param {Array<string>=} params.contexts
   *     Optional list of top-level browsing context ids
   *     to subscribe the events for.
   *
   * @throws {InvalidArgumentError}
   *     If <var>events</var> or <var>contexts</var> are not valid types.
   */
  async subscribe(params = {}) {
    const { events, contexts: contextIds = null } = params;

    // Check input types until we run schema validation.
    this.#assertNonEmptyArrayWithStrings(events, "events");

    if (contextIds !== null) {
      this.#assertNonEmptyArrayWithStrings(contextIds, "contexts");
    }

    const listeners = this.#updateEventMap(events, contextIds, true);

    // TODO: Bug 1801284. Add subscribe priority sorting of subscribeStepEvents (step 4 to 6, and 8).

    // Subscribe to the relevant engine-internal events.
    await this.messageHandler.eventsDispatcher.update(listeners);
  }

  /**
   * Disable certain events either globally, or for a list of browsing contexts.
   *
   * @param {object=} params
   * @param {Array<string>} params.events
   *     List of events to unsubscribe from.
   * @param {Array<string>=} params.contexts
   *     Optional list of top-level browsing context ids
   *     to unsubscribe the events from.
   *
   * @throws {InvalidArgumentError}
   *     If <var>events</var> or <var>contexts</var> are not valid types.
   */
  async unsubscribe(params = {}) {
    const { events, contexts: contextIds = null } = params;

    // Check input types until we run schema validation.
    this.#assertNonEmptyArrayWithStrings(events, "events");
    if (contextIds !== null) {
      this.#assertNonEmptyArrayWithStrings(contextIds, "contexts");
    }

    const listeners = this.#updateEventMap(events, contextIds, false);

    // Unsubscribe from the relevant engine-internal events.
    await this.messageHandler.eventsDispatcher.update(listeners);
  }

  #assertModuleSupportsEvent(moduleName, event) {
    const rootModuleClass = this.#getRootModuleClass(moduleName);
    if (!rootModuleClass?.supportsEvent(event)) {
      throw new lazy.error.InvalidArgumentError(
        `${event} is not a valid event name`
      );
    }
  }

  #assertNonEmptyArrayWithStrings(array, variableName) {
    lazy.assert.array(
      array,
      `Expected "${variableName}" to be an array, got ${array}`
    );
    lazy.assert.that(
      array => !!array.length,
      `Expected "${variableName}" array to have at least one item`
    )(array);
    array.forEach(item => {
      lazy.assert.string(
        item,
        `Expected elements of "${variableName}" to be a string, got ${item}`
      );
    });
  }

  #getBrowserIdForContextId(contextId) {
    const context = lazy.TabManager.getBrowsingContextById(contextId);
    if (!context) {
      throw new lazy.error.NoSuchFrameError(
        `Browsing context with id ${contextId} not found`
      );
    }

    return context.browserId;
  }

  #getRootModuleClass(moduleName) {
    // Modules which support event subscriptions should have a root module
    // defining supported events.
    const rootDestination = { type: lazy.RootMessageHandler.type };
    const moduleClasses = this.messageHandler.getAllModuleClasses(
      moduleName,
      rootDestination
    );

    if (!moduleClasses.length) {
      throw new lazy.error.InvalidArgumentError(
        `Module ${moduleName} does not exist`
      );
    }

    return moduleClasses[0];
  }

  #getTopBrowsingContextId(contextId) {
    const context = lazy.TabManager.getBrowsingContextById(contextId);
    if (!context) {
      throw new lazy.error.NoSuchFrameError(
        `Browsing context with id ${contextId} not found`
      );
    }
    const topContext = context.top;
    return lazy.TabManager.getIdForBrowsingContext(topContext);
  }

  /**
   * Obtain a set of events based on the given event name.
   *
   * Could contain a period for a specific event,
   * or just the module name for all events.
   *
   * @param {string} event
   *     Name of the event to process.
   *
   * @returns {Set<string>}
   *     A Set with the expanded events in the form of `<module>.<event>`.
   *
   * @throws {InvalidArgumentError}
   *     If <var>event</var> does not reference a valid event.
   */
  #obtainEvents(event) {
    const events = new Set();

    // Check if a period is present that splits the event name into the module,
    // and the actual event. Hereby only care about the first found instance.
    const index = event.indexOf(".");
    if (index >= 0) {
      const [moduleName] = event.split(".");
      this.#assertModuleSupportsEvent(moduleName, event);
      events.add(event);
    } else {
      // Interpret the name as module, and register all its available events
      const rootModuleClass = this.#getRootModuleClass(event);
      const supportedEvents = rootModuleClass?.supportedEvents;

      for (const eventName of supportedEvents) {
        events.add(eventName);
      }
    }

    return events;
  }

  /**
   * Obtain a list of event enabled browsing context ids.
   *
   * @see https://w3c.github.io/webdriver-bidi/#event-enabled-browsing-contexts
   *
   * @param {string} eventName
   *     The name of the event.
   *
   * @returns {Set<string>} The set of browsing context.
   */
  #obtainEventEnabledBrowsingContextIds(eventName) {
    const contextIds = new Set();
    for (const [
      contextId,
      events,
    ] of this.#browsingContextIdEventMap.entries()) {
      if (events.has(eventName)) {
        // Check that a browsing context still exists for a given id
        const context = lazy.TabManager.getBrowsingContextById(contextId);
        if (context) {
          contextIds.add(contextId);
        }
      }
    }

    return contextIds;
  }

  #onMessageHandlerEvent = (name, event) => {
    this.messageHandler.emitProtocolEvent(name, event);
  };

  /**
   * Update global event state for top-level browsing contexts.
   *
   * @see https://w3c.github.io/webdriver-bidi/#update-the-event-map
   *
   * @param {Array<string>} requestedEventNames
   *     The list of the event names to run the update for.
   * @param {Array<string>|null} browsingContextIds
   *     The list of the browsing context ids to update or null.
   * @param {boolean} enabled
   *     True, if events have to be enabled. Otherwise false.
   *
   * @returns {Array<Subscription>} subscriptions
   *     The list of information to subscribe/unsubscribe to.
   *
   * @throws {InvalidArgumentError}
   *     If failed unsubscribe from event from <var>requestedEventNames</var> for
   *     browsing context id from <var>browsingContextIds</var>, if present.
   */
  #updateEventMap(requestedEventNames, browsingContextIds, enabled) {
    const globalEventSet = new Set(this.#globalEventSet);
    const eventMap = structuredClone(this.#browsingContextIdEventMap);

    const eventNames = new Set();

    requestedEventNames.forEach(name => {
      this.#obtainEvents(name).forEach(event => eventNames.add(event));
    });
    const enabledEvents = new Map();
    const subscriptions = [];

    if (browsingContextIds === null) {
      // Subscribe or unsubscribe events for all browsing contexts.
      if (enabled) {
        // Subscribe to each event.

        // Get the list of all top level browsing context ids.
        const allTopBrowsingContextIds = lazy.TabManager.allBrowserUniqueIds;

        for (const eventName of eventNames) {
          if (!globalEventSet.has(eventName)) {
            const alreadyEnabledContextIds =
              this.#obtainEventEnabledBrowsingContextIds(eventName);
            globalEventSet.add(eventName);
            for (const contextId of alreadyEnabledContextIds) {
              eventMap.get(contextId).delete(eventName);

              // Since we're going to subscribe to all top-level
              // browsing context ids to not have duplicate subscriptions,
              // we have to unsubscribe from already subscribed.
              subscriptions.push({
                event: eventName,
                contextDescriptor: {
                  type: lazy.ContextDescriptorType.TopBrowsingContext,
                  id: this.#getBrowserIdForContextId(contextId),
                },
                callback: this.#onMessageHandlerEvent,
                enable: false,
              });
            }

            // Get a list of all top-level browsing context ids
            // that are not contained in alreadyEnabledContextIds.
            const newlyEnabledContextIds = allTopBrowsingContextIds.filter(
              contextId => !alreadyEnabledContextIds.has(contextId)
            );

            enabledEvents.set(eventName, newlyEnabledContextIds);

            subscriptions.push({
              event: eventName,
              contextDescriptor: {
                type: lazy.ContextDescriptorType.All,
              },
              callback: this.#onMessageHandlerEvent,
              enable: true,
            });
          }
        }
      } else {
        // Unsubscribe each event which has a global subscription.
        for (const eventName of eventNames) {
          if (globalEventSet.has(eventName)) {
            globalEventSet.delete(eventName);

            subscriptions.push({
              event: eventName,
              contextDescriptor: {
                type: lazy.ContextDescriptorType.All,
              },
              callback: this.#onMessageHandlerEvent,
              enable: false,
            });
          } else {
            throw new lazy.error.InvalidArgumentError(
              `Failed to unsubscribe from event ${eventName}`
            );
          }
        }
      }
    } else {
      // Subscribe or unsubscribe events for given list of browsing context ids.
      const targets = new Map();
      for (const contextId of browsingContextIds) {
        const topLevelContextId = this.#getTopBrowsingContextId(contextId);
        if (!eventMap.has(topLevelContextId)) {
          eventMap.set(topLevelContextId, new Set());
        }
        targets.set(topLevelContextId, eventMap.get(topLevelContextId));
      }

      for (const eventName of eventNames) {
        // Do nothing if we want to subscribe,
        // but the event has already a global subscription.
        if (enabled && this.#globalEventSet.has(eventName)) {
          continue;
        }
        for (const [contextId, target] of targets.entries()) {
          // Subscribe if an event doesn't have a subscription for a specific context id.
          if (enabled && !target.has(eventName)) {
            target.add(eventName);
            if (!enabledEvents.has(eventName)) {
              enabledEvents.set(eventName, new Set());
            }
            enabledEvents.get(eventName).add(contextId);

            subscriptions.push({
              event: eventName,
              contextDescriptor: {
                type: lazy.ContextDescriptorType.TopBrowsingContext,
                id: this.#getBrowserIdForContextId(contextId),
              },
              callback: this.#onMessageHandlerEvent,
              enable: true,
            });
          } else if (!enabled) {
            // Unsubscribe from each event for a specific context id if the event has a subscription.
            if (target.has(eventName)) {
              target.delete(eventName);

              subscriptions.push({
                event: eventName,
                contextDescriptor: {
                  type: lazy.ContextDescriptorType.TopBrowsingContext,
                  id: this.#getBrowserIdForContextId(contextId),
                },
                callback: this.#onMessageHandlerEvent,
                enable: false,
              });
            } else {
              throw new lazy.error.InvalidArgumentError(
                `Failed to unsubscribe from event ${eventName} for context ${contextId}`
              );
            }
          }
        }
      }
    }

    this.#globalEventSet = globalEventSet;
    this.#browsingContextIdEventMap = eventMap;

    return subscriptions;
  }
}

// To export the class as lower-case
export const session = SessionModule;