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 = ["Page"];
8
9var { XPCOMUtils } = ChromeUtils.import(
10  "resource://gre/modules/XPCOMUtils.jsm"
11);
12
13const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14
15XPCOMUtils.defineLazyModuleGetters(this, {
16  SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
17});
18
19const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
20const { clearInterval, setInterval } = ChromeUtils.import(
21  "resource://gre/modules/Timer.jsm"
22);
23
24const { DialogHandler } = ChromeUtils.import(
25  "chrome://remote/content/domains/parent/page/DialogHandler.jsm"
26);
27const { Domain } = ChromeUtils.import(
28  "chrome://remote/content/domains/Domain.jsm"
29);
30const { UnsupportedError } = ChromeUtils.import(
31  "chrome://remote/content/Error.jsm"
32);
33const { streamRegistry } = ChromeUtils.import(
34  "chrome://remote/content/domains/parent/IO.jsm"
35);
36const { PollPromise } = ChromeUtils.import("chrome://remote/content/Sync.jsm");
37const { TabManager } = ChromeUtils.import(
38  "chrome://remote/content/TabManager.jsm"
39);
40const { WindowManager } = ChromeUtils.import(
41  "chrome://remote/content/WindowManager.jsm"
42);
43
44const MAX_CANVAS_DIMENSION = 32767;
45const MAX_CANVAS_AREA = 472907776;
46
47const PRINT_MAX_SCALE_VALUE = 2.0;
48const PRINT_MIN_SCALE_VALUE = 0.1;
49
50const PDF_TRANSFER_MODES = {
51  base64: "ReturnAsBase64",
52  stream: "ReturnAsStream",
53};
54
55const TIMEOUT_SET_HISTORY_INDEX = 1000;
56
57class Page extends Domain {
58  constructor(session) {
59    super(session);
60
61    this._onDialogLoaded = this._onDialogLoaded.bind(this);
62    this._onRequest = this._onRequest.bind(this);
63
64    this.enabled = false;
65    this.session.networkObserver.startTrackingBrowserNetwork(
66      this.session.target.browser
67    );
68    this.session.networkObserver.on("request", this._onRequest);
69  }
70
71  destructor() {
72    // Flip a flag to avoid to disable the content domain from this.disable()
73    this._isDestroyed = false;
74    this.disable();
75
76    this.session.networkObserver.off("request", this._onRequest);
77    this.session.networkObserver.stopTrackingBrowserNetwork(
78      this.session.target.browser
79    );
80    super.destructor();
81  }
82
83  // commands
84
85  /**
86   * Navigates current page to given URL.
87   *
88   * @param {Object} options
89   * @param {string} options.url
90   *     destination URL
91   * @param {string=} options.frameId
92   *     frame id to navigate (not supported),
93   *     if not specified navigate top frame
94   * @param {string=} options.referrer
95   *     referred URL (optional)
96   * @param {string=} options.transitionType
97   *     intended transition type
98   * @return {Object}
99   *         - frameId {string} frame id that has navigated (or failed to)
100   *         - errorText {string=} error message if navigation has failed
101   *         - loaderId {string} (not supported)
102   */
103  async navigate(options = {}) {
104    const { url, frameId, referrer, transitionType } = options;
105    if (typeof url != "string") {
106      throw new TypeError("url: string value expected");
107    }
108    let validURL;
109    try {
110      validURL = Services.io.newURI(url);
111    } catch (e) {
112      throw new Error("Error: Cannot navigate to invalid URL");
113    }
114    const topFrameId = this.session.browsingContext.id.toString();
115    if (frameId && frameId != topFrameId) {
116      throw new UnsupportedError("frameId not supported");
117    }
118
119    const requestDone = new Promise(resolve => {
120      if (!["https", "http"].includes(validURL.scheme)) {
121        resolve({});
122        return;
123      }
124      let navigationRequestId, redirectedRequestId;
125      const _onNavigationRequest = function(_type, _ch, data) {
126        const {
127          url: requestURL,
128          requestId,
129          redirectedFrom = null,
130          isNavigationRequest,
131        } = data;
132        if (!isNavigationRequest) {
133          return;
134        }
135        if (validURL.spec === requestURL) {
136          navigationRequestId = redirectedRequestId = requestId;
137        } else if (redirectedFrom === redirectedRequestId) {
138          redirectedRequestId = requestId;
139        }
140      };
141
142      const _onRequestFinished = function(_type, _ch, data) {
143        const { requestId, errorCode } = data;
144        if (
145          redirectedRequestId !== requestId ||
146          errorCode == "NS_BINDING_REDIRECTED"
147        ) {
148          // handle next request in redirection chain
149          return;
150        }
151        this.session.networkObserver.off("request", _onNavigationRequest);
152        this.session.networkObserver.off("requestfinished", _onRequestFinished);
153        resolve({ errorCode, navigationRequestId });
154      }.bind(this);
155
156      this.session.networkObserver.on("request", _onNavigationRequest);
157      this.session.networkObserver.on("requestfinished", _onRequestFinished);
158    });
159
160    const opts = {
161      loadFlags: transitionToLoadFlag(transitionType),
162      referrerURI: referrer,
163      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
164    };
165    this.session.browsingContext.loadURI(url, opts);
166    // clients expect loaderId == requestId for a document navigation request
167    const { navigationRequestId: loaderId, errorCode } = await requestDone;
168    const result = {
169      frameId: topFrameId,
170      loaderId,
171    };
172    if (errorCode) {
173      result.errorText = errorCode;
174    }
175    return result;
176  }
177
178  /**
179   * Capture page screenshot.
180   *
181   * @param {Object} options
182   * @param {Viewport=} options.clip
183   *     Capture the screenshot of a given region only.
184   * @param {string=} options.format
185   *     Image compression format. Defaults to "png".
186   * @param {number=} options.quality
187   *     Compression quality from range [0..100] (jpeg only). Defaults to 80.
188   *
189   * @return {string}
190   *     Base64-encoded image data.
191   */
192  async captureScreenshot(options = {}) {
193    const { clip, format = "png", quality = 80 } = options;
194
195    if (options.fromSurface) {
196      throw new UnsupportedError("fromSurface not supported");
197    }
198
199    let rect;
200    let scale = await this.executeInChild("_devicePixelRatio");
201
202    if (clip) {
203      for (const prop of ["x", "y", "width", "height", "scale"]) {
204        if (clip[prop] == undefined) {
205          throw new TypeError(`clip.${prop}: double value expected`);
206        }
207      }
208
209      const contentRect = await this.executeInChild("_contentRect");
210
211      // For invalid scale values default to full page
212      if (clip.scale <= 0) {
213        Object.assign(clip, {
214          x: 0,
215          y: 0,
216          width: contentRect.width,
217          height: contentRect.height,
218          scale: 1,
219        });
220      } else {
221        if (clip.x < 0 || clip.x > contentRect.width - 1) {
222          clip.x = 0;
223        }
224        if (clip.y < 0 || clip.y > contentRect.height - 1) {
225          clip.y = 0;
226        }
227        if (clip.width <= 0) {
228          clip.width = contentRect.width;
229        }
230        if (clip.height <= 0) {
231          clip.height = contentRect.height;
232        }
233      }
234
235      rect = new DOMRect(clip.x, clip.y, clip.width, clip.height);
236      scale *= clip.scale;
237    } else {
238      // If no specific clipping region has been specified,
239      // fallback to the layout (fixed) viewport, and the
240      // default pixel ratio.
241      const {
242        pageX,
243        pageY,
244        clientWidth,
245        clientHeight,
246      } = await this.executeInChild("_layoutViewport");
247
248      rect = new DOMRect(pageX, pageY, clientWidth, clientHeight);
249    }
250
251    let canvasWidth = rect.width * scale;
252    let canvasHeight = rect.height * scale;
253
254    // Cap the screenshot size based on maximum allowed canvas sizes.
255    // Using higher dimensions would trigger exceptions in Gecko.
256    //
257    // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#Maximum_canvas_size
258    if (canvasWidth > MAX_CANVAS_DIMENSION) {
259      rect.width = Math.floor(MAX_CANVAS_DIMENSION / scale);
260      canvasWidth = rect.width * scale;
261    }
262    if (canvasHeight > MAX_CANVAS_DIMENSION) {
263      rect.height = Math.floor(MAX_CANVAS_DIMENSION / scale);
264      canvasHeight = rect.height * scale;
265    }
266    // If the area is larger, reduce the height to keep the full width.
267    if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) {
268      rect.height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale));
269      canvasHeight = rect.height * scale;
270    }
271
272    const { browsingContext, window } = this.session.target;
273    const snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
274      rect,
275      scale,
276      "rgb(255,255,255)"
277    );
278
279    const canvas = window.document.createElementNS(
280      "http://www.w3.org/1999/xhtml",
281      "canvas"
282    );
283    canvas.width = canvasWidth;
284    canvas.height = canvasHeight;
285
286    const ctx = canvas.getContext("2d");
287    ctx.drawImage(snapshot, 0, 0);
288
289    // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies
290    // of the bitmap will exist in memory. Force the removal of the snapshot
291    // because it is no longer needed.
292    snapshot.close();
293
294    const url = canvas.toDataURL(`image/${format}`, quality / 100);
295    if (!url.startsWith(`data:image/${format}`)) {
296      throw new UnsupportedError(`Unsupported MIME type: image/${format}`);
297    }
298
299    // only return the base64 encoded data without the data URL prefix
300    const data = url.substring(url.indexOf(",") + 1);
301
302    return { data };
303  }
304
305  async enable() {
306    if (this.enabled) {
307      return;
308    }
309
310    this.enabled = true;
311
312    const { browser } = this.session.target;
313    this._dialogHandler = new DialogHandler(browser);
314    this._dialogHandler.on("dialog-loaded", this._onDialogLoaded);
315    await this.executeInChild("enable");
316  }
317
318  async disable() {
319    if (!this.enabled) {
320      return;
321    }
322
323    this._dialogHandler.destructor();
324    this._dialogHandler = null;
325    this.enabled = false;
326
327    if (!this._isDestroyed) {
328      // Only call disable in the content domain if we are not destroying the domain.
329      // If we are destroying the domain, the content domains will be destroyed
330      // independently after firing the remote:destroy event.
331      await this.executeInChild("disable");
332    }
333  }
334
335  async bringToFront() {
336    const { tab, window } = this.session.target;
337
338    // Focus the window, and select the corresponding tab
339    await WindowManager.focus(window);
340    TabManager.selectTab(tab);
341  }
342
343  /**
344   * Return metrics relating to the layouting of the page.
345   *
346   * The returned object contains the following entries:
347   *
348   * layoutViewport:
349   *     {number} pageX
350   *         Horizontal offset relative to the document (CSS pixels)
351   *     {number} pageY
352   *         Vertical offset relative to the document (CSS pixels)
353   *     {number} clientWidth
354   *         Width (CSS pixels), excludes scrollbar if present
355   *     {number} clientHeight
356   *         Height (CSS pixels), excludes scrollbar if present
357   *
358   * visualViewport:
359   *     {number} offsetX
360   *         Horizontal offset relative to the layout viewport (CSS pixels)
361   *     {number} offsetY
362   *         Vertical offset relative to the layout viewport (CSS pixels)
363   *     {number} pageX
364   *         Horizontal offset relative to the document (CSS pixels)
365   *     {number} pageY
366   *         Vertical offset relative to the document (CSS pixels)
367   *     {number} clientWidth
368   *         Width (CSS pixels), excludes scrollbar if present
369   *     {number} clientHeight
370   *         Height (CSS pixels), excludes scrollbar if present
371   *     {number} scale
372   *         Scale relative to the ideal viewport (size at width=device-width)
373   *     {number} zoom
374   *         Page zoom factor (CSS to device independent pixels ratio)
375   *
376   * contentSize:
377   *     {number} x
378   *         X coordinate
379   *     {number} y
380   *         Y coordinate
381   *     {number} width
382   *         Width of scrollable area
383   *     {number} height
384   *         Height of scrollable area
385   *
386   * @return {Promise}
387   * @resolves {layoutViewport, visualViewport, contentSize}
388   */
389  async getLayoutMetrics() {
390    return {
391      layoutViewport: await this.executeInChild("_layoutViewport"),
392      contentSize: await this.executeInChild("_contentRect"),
393    };
394  }
395
396  /**
397   * Returns navigation history for the current page.
398   *
399   * @return {currentIndex:number, entries:Array<NavigationEntry>}
400   */
401  async getNavigationHistory() {
402    const { window } = this.session.target;
403
404    return new Promise(resolve => {
405      function updateSessionHistory(sessionHistory) {
406        const entries = sessionHistory.entries.map(entry => {
407          return {
408            id: entry.ID,
409            url: entry.url,
410            userTypedURL: entry.originalURI || entry.url,
411            title: entry.title,
412            // TODO: Bug 1609514
413            transitionType: null,
414          };
415        });
416
417        resolve({
418          currentIndex: sessionHistory.index,
419          entries,
420        });
421      }
422
423      SessionStore.getSessionHistory(
424        window.gBrowser.selectedTab,
425        updateSessionHistory
426      );
427    });
428  }
429
430  /**
431   * Interact with the currently opened JavaScript dialog (alert, confirm,
432   * prompt) for this page. This will always close the dialog, either accepting
433   * or rejecting it, with the optional prompt filled.
434   *
435   * @param {Object}
436   *        - {Boolean} accept: For "confirm", "prompt", "beforeunload" dialogs
437   *          true will accept the dialog, false will cancel it. For "alert"
438   *          dialogs, true or false closes the dialog in the same way.
439   *        - {String} promptText: for "prompt" dialogs, used to fill the prompt
440   *          input.
441   */
442  async handleJavaScriptDialog({ accept, promptText }) {
443    if (!this.enabled) {
444      throw new Error("Page domain is not enabled");
445    }
446    await this._dialogHandler.handleJavaScriptDialog({ accept, promptText });
447  }
448
449  /**
450   * Navigates current page to the given history entry.
451   *
452   * @param {Object} options
453   * @param {number} options.entryId
454   *    Unique id of the entry to navigate to.
455   */
456  async navigateToHistoryEntry(options = {}) {
457    const { entryId } = options;
458
459    const index = await this._getIndexForHistoryEntryId(entryId);
460
461    if (index == null) {
462      throw new Error("No entry with passed id");
463    }
464
465    const { window } = this.session.target;
466    window.gBrowser.gotoIndex(index);
467
468    // On some platforms the requested index isn't set immediately.
469    await PollPromise(
470      async (resolve, reject) => {
471        const currentIndex = await this._getCurrentHistoryIndex();
472        if (currentIndex == index) {
473          resolve();
474        } else {
475          reject();
476        }
477      },
478      { timeout: TIMEOUT_SET_HISTORY_INDEX }
479    );
480  }
481
482  /**
483   * Print page as PDF.
484   *
485   * @param {Object} options
486   * @param {boolean=} options.displayHeaderFooter
487   *     Display header and footer. Defaults to false.
488   * @param {string=} options.footerTemplate (not supported)
489   *     HTML template for the print footer.
490   * @param {string=} options.headerTemplate (not supported)
491   *     HTML template for the print header. Should use the same format
492   *     as the footerTemplate.
493   * @param {boolean=} options.ignoreInvalidPageRanges
494   *     Whether to silently ignore invalid but successfully parsed page ranges,
495   *     such as '3-2'. Defaults to false.
496   * @param {boolean=} options.landscape
497   *     Paper orientation. Defaults to false.
498   * @param {number=} options.marginBottom
499   *     Bottom margin in inches. Defaults to 1cm (~0.4 inches).
500   * @param {number=} options.marginLeft
501   *     Left margin in inches. Defaults to 1cm (~0.4 inches).
502   * @param {number=} options.marginRight
503   *     Right margin in inches. Defaults to 1cm (~0.4 inches).
504   * @param {number=} options.marginTop
505   *     Top margin in inches. Defaults to 1cm (~0.4 inches).
506   * @param {string=} options.pageRanges (not supported)
507   *     Paper ranges to print, e.g., '1-5, 8, 11-13'.
508   *     Defaults to the empty string, which means print all pages.
509   * @param {number=} options.paperHeight
510   *     Paper height in inches. Defaults to 11 inches.
511   * @param {number=} options.paperWidth
512   *     Paper width in inches. Defaults to 8.5 inches.
513   * @param {boolean=} options.preferCSSPageSize
514   *     Whether or not to prefer page size as defined by CSS.
515   *     Defaults to false, in which case the content will be scaled
516   *     to fit the paper size.
517   * @param {boolean=} options.printBackground
518   *     Print background graphics. Defaults to false.
519   * @param {number=} options.scale
520   *     Scale of the webpage rendering. Defaults to 1.
521   * @param {string=} options.transferMode
522   *     Return as base64-encoded string (ReturnAsBase64),
523   *     or stream (ReturnAsStream). Defaults to ReturnAsBase64.
524   *
525   * @return {Promise<{data:string, stream:string}>
526   *     Based on the transferMode setting data is a base64-encoded string,
527   *     or stream is a handle to a OS.File stream.
528   */
529  async printToPDF(options = {}) {
530    const {
531      displayHeaderFooter = false,
532      // Bug 1601570 - Implement templates for header and footer
533      // headerTemplate = "",
534      // footerTemplate = "",
535      landscape = false,
536      marginBottom = 0.39,
537      marginLeft = 0.39,
538      marginRight = 0.39,
539      marginTop = 0.39,
540      // Bug 1601571 - Implement handling of page ranges
541      // TODO: pageRanges = "",
542      // TODO: ignoreInvalidPageRanges = false,
543      paperHeight = 11.0,
544      paperWidth = 8.5,
545      preferCSSPageSize = false,
546      printBackground = false,
547      scale = 1.0,
548      transferMode = PDF_TRANSFER_MODES.base64,
549    } = options;
550
551    if (marginBottom < 0) {
552      throw new TypeError("marginBottom is negative");
553    }
554    if (marginLeft < 0) {
555      throw new TypeError("marginLeft is negative");
556    }
557    if (marginRight < 0) {
558      throw new TypeError("marginRight is negative");
559    }
560    if (marginTop < 0) {
561      throw new TypeError("marginTop is negative");
562    }
563    if (scale < PRINT_MIN_SCALE_VALUE || scale > PRINT_MAX_SCALE_VALUE) {
564      throw new TypeError("scale is outside [0.1 - 2] range");
565    }
566    if (paperHeight <= 0) {
567      throw new TypeError("paperHeight is zero or negative");
568    }
569    if (paperWidth <= 0) {
570      throw new TypeError("paperWidth is zero or negative");
571    }
572
573    // Create a unique filename for the temporary PDF file
574    const basePath = OS.Path.join(OS.Constants.Path.tmpDir, "remote-agent.pdf");
575    const { file, path: filePath } = await OS.File.openUnique(basePath);
576    await file.close();
577
578    const psService = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
579      Ci.nsIPrintSettingsService
580    );
581
582    const printSettings = psService.newPrintSettings;
583    printSettings.isInitializedFromPrinter = true;
584    printSettings.isInitializedFromPrefs = true;
585    printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
586    printSettings.printerName = "";
587    printSettings.printSilent = true;
588    printSettings.printToFile = true;
589    printSettings.showPrintProgress = false;
590    printSettings.toFileName = filePath;
591
592    printSettings.paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches;
593    printSettings.paperWidth = paperWidth;
594    printSettings.paperHeight = paperHeight;
595
596    printSettings.marginBottom = marginBottom;
597    printSettings.marginLeft = marginLeft;
598    printSettings.marginRight = marginRight;
599    printSettings.marginTop = marginTop;
600
601    printSettings.printBGColors = printBackground;
602    printSettings.printBGImages = printBackground;
603    printSettings.scaling = scale;
604    printSettings.shrinkToFit = preferCSSPageSize;
605
606    if (!displayHeaderFooter) {
607      printSettings.headerStrCenter = "";
608      printSettings.headerStrLeft = "";
609      printSettings.headerStrRight = "";
610      printSettings.footerStrCenter = "";
611      printSettings.footerStrLeft = "";
612      printSettings.footerStrRight = "";
613    }
614
615    if (landscape) {
616      printSettings.orientation = Ci.nsIPrintSettings.kLandscapeOrientation;
617    }
618
619    await new Promise(resolve => {
620      // Bug 1603739 - With e10s enabled the WebProgressListener states
621      // STOP too early, which means the file hasn't been completely written.
622      const waitForFileWritten = () => {
623        const DELAY_CHECK_FILE_COMPLETELY_WRITTEN = 100;
624
625        let lastSize = 0;
626        const timerId = setInterval(async () => {
627          const fileInfo = await OS.File.stat(filePath);
628          if (lastSize > 0 && fileInfo.size == lastSize) {
629            clearInterval(timerId);
630            resolve();
631          }
632          lastSize = fileInfo.size;
633        }, DELAY_CHECK_FILE_COMPLETELY_WRITTEN);
634      };
635
636      const printProgressListener = {
637        onStateChange(webProgress, request, flags, status) {
638          if (
639            flags & Ci.nsIWebProgressListener.STATE_STOP &&
640            flags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
641          ) {
642            waitForFileWritten();
643          }
644        },
645        QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener]),
646      };
647
648      const { tab } = this.session.target;
649      tab.linkedBrowser.print(
650        tab.linkedBrowser.outerWindowID,
651        printSettings,
652        printProgressListener
653      );
654    });
655
656    const fp = await OS.File.open(filePath);
657
658    const retval = { data: null, stream: null };
659    if (transferMode == PDF_TRANSFER_MODES.stream) {
660      retval.stream = streamRegistry.add(fp);
661    } else {
662      // return all data as a base64 encoded string
663      let bytes;
664      try {
665        bytes = await fp.read();
666      } finally {
667        fp.close();
668        await OS.File.remove(filePath);
669      }
670
671      // Each UCS2 character has an upper byte of 0 and a lower byte matching
672      // the binary data
673      retval.data = btoa(String.fromCharCode.apply(null, bytes));
674    }
675
676    return retval;
677  }
678
679  /**
680   * Intercept file chooser requests and transfer control to protocol clients.
681   *
682   * When file chooser interception is enabled,
683   * the native file chooser dialog is not shown.
684   * Instead, a protocol event Page.fileChooserOpened is emitted.
685   *
686   * @param {Object} options
687   * @param {boolean=} options.enabled
688   *     Enabled state of file chooser interception.
689   */
690  setInterceptFileChooserDialog(options = {}) {}
691
692  _getCurrentHistoryIndex() {
693    const { window } = this.session.target;
694
695    return new Promise(resolve => {
696      SessionStore.getSessionHistory(window.gBrowser.selectedTab, history => {
697        resolve(history.index);
698      });
699    });
700  }
701
702  _getIndexForHistoryEntryId(id) {
703    const { window } = this.session.target;
704
705    return new Promise(resolve => {
706      function updateSessionHistory(sessionHistory) {
707        sessionHistory.entries.forEach((entry, index) => {
708          if (entry.ID == id) {
709            resolve(index);
710          }
711        });
712
713        resolve(null);
714      }
715
716      SessionStore.getSessionHistory(
717        window.gBrowser.selectedTab,
718        updateSessionHistory
719      );
720    });
721  }
722
723  /**
724   * Emit the proper CDP event javascriptDialogOpening when a javascript dialog
725   * opens for the current target.
726   */
727  _onDialogLoaded(e, data) {
728    const { message, type } = data;
729    // XXX: We rely on the tabmodal-dialog-loaded event (see DialogHandler.jsm)
730    // which is inconsistent with the name "javascriptDialogOpening".
731    // For correctness we should rely on an event fired _before_ the prompt is
732    // visible, such as DOMWillOpenModalDialog. However the payload of this
733    // event does not contain enough data to populate javascriptDialogOpening.
734    //
735    // Since the event is fired asynchronously, this should not have an impact
736    // on the actual tests relying on this API.
737    this.emit("Page.javascriptDialogOpening", { message, type });
738  }
739
740  /**
741   * Handles HTTP request to propagate loaderId to events emitted from
742   * content process
743   */
744  _onRequest(_type, _ch, data) {
745    if (!data.loaderId) {
746      return;
747    }
748    this.executeInChild("_updateLoaderId", {
749      loaderId: data.loaderId,
750      frameId: data.frameId,
751    });
752  }
753}
754
755function transitionToLoadFlag(transitionType) {
756  switch (transitionType) {
757    case "reload":
758      return Ci.nsIWebNavigation.LOAD_FLAGS_IS_REFRESH;
759    case "link":
760    default:
761      return Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK;
762  }
763}
764