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