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