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