Bug 1025146 - Modernize viewSource.js to use a frame script. r=jryans.
authorMike Conley <mconley@mozilla.com>
Fri, 24 Apr 2015 17:32:57 -0400
changeset 243275 13bf99d217bf4485bda6a77a7d93b6c9b3618acf
parent 243274 5fe8e7a205e6062b23feb017be3327bb98a540cf
child 243276 28c41c53d0c24528bd3c7050a93939189c3b3a83
push id28736
push usercbook@mozilla.com
push dateTue, 12 May 2015 10:01:42 +0000
treeherdermozilla-central@0ca37b3cb73d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjryans
bugs1025146
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1025146 - Modernize viewSource.js to use a frame script. r=jryans.
browser/base/content/browser-sets.inc
browser/base/content/browser.js
toolkit/components/viewsource/content/viewPartialSource.js
toolkit/components/viewsource/content/viewPartialSource.xul
toolkit/components/viewsource/content/viewSource-content.js
toolkit/components/viewsource/content/viewSource.css
toolkit/components/viewsource/content/viewSource.js
toolkit/components/viewsource/content/viewSource.xul
toolkit/components/viewsource/jar.mn
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -33,17 +33,17 @@
     <command id="cmd_close" oncommand="BrowserCloseTabOrWindow()" reserved="true"/>
     <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow()" reserved="true"/>
     <command id="cmd_CustomizeToolbars" oncommand="BrowserCustomizeToolbar()"/>
     <command id="cmd_quitApplication" oncommand="goQuitApplication()" reserved="true"/>
 
 
     <commandset id="editMenuCommands"/>
 
-    <command id="View:PageSource" oncommand="BrowserViewSourceOfDocument(window.gBrowser.selectedBrowser.contentDocumentAsCPOW);" observes="isImage"/>
+    <command id="View:PageSource" oncommand="BrowserViewSource(window.gBrowser.selectedBrowser);" observes="isImage"/>
     <command id="View:PageInfo" oncommand="BrowserPageInfo();"/>
     <command id="View:FullScreen" oncommand="BrowserFullScreen();"/>
     <command id="View:ReaderView" oncommand="ReaderParent.toggleReaderMode(event);"/>
     <command id="cmd_find"
              oncommand="gFindBar.onFindCommand();"
              observes="isImage"/>
     <command id="cmd_findAgain"
              oncommand="gFindBar.onFindAgainCommand(false);"
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2277,72 +2277,81 @@ function readFromClipboard()
       url = data.data.substring(0, dataLen.value / 2);
     }
   } catch (ex) {
   }
 
   return url;
 }
 
-function BrowserViewSourceOfDocument(aDocument)
-{
-  var pageCookie;
-  var webNav;
-
-  // Get the document charset
-  var docCharset = "charset=" + aDocument.characterSet;
-
-  // Get the nsIWebNavigation associated with the document
-  try {
-      var win;
-      var ifRequestor;
-
-      // Get the DOMWindow for the requested document.  If the DOMWindow
-      // cannot be found, then just use the content window...
-      //
-      // XXX:  This is a bit of a hack...
-      win = aDocument.defaultView;
-      if (win == window) {
-        win = content;
-      }
-      ifRequestor = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
-
-      webNav = ifRequestor.getInterface(nsIWebNavigation);
-  } catch(err) {
-      // If nsIWebNavigation cannot be found, just get the one for the whole
-      // window...
-      webNav = gBrowser.webNavigation;
-  }
-  //
-  // Get the 'PageDescriptor' for the current document. This allows the
-  // view-source to access the cached copy of the content rather than
-  // refetching it from the network...
-  //
-  try {
-
-#ifdef E10S_TESTING_ONLY
-    // Workaround for bug 988133, which causes a crash if we attempt to load
-    // the document from the cache when the document is a CPOW (which occurs
-    // if we're using remote tabs). This causes us to reload the document from
-    // the network in this case, so it's not a permanent solution, hence hiding
-    // it behind the E10S_TESTING_ONLY ifdef. This is just a band-aid fix until
-    // we can find something better - see bug 1025146.
-    if (!Cu.isCrossProcessWrapper(aDocument)) {
-#endif
-      var PageLoader = webNav.QueryInterface(Components.interfaces.nsIWebPageDescriptor);
-
-      pageCookie = PageLoader.currentDescriptor;
-#ifdef E10S_TESTING_ONLY
-    }
-#endif
-  } catch(err) {
-    // If no page descriptor is available, just use the view-source URL...
-  }
-
-  top.gViewSourceUtils.viewSource(webNav.currentURI.spec, pageCookie, aDocument);
+/**
+ * Open the View Source dialog.
+ *
+ * @param aArgsOrDocument
+ *        Either an object or a Document. Passing a Document is deprecated,
+ *        and is not supported with e10s. This function will throw if
+ *        aArgsOrDocument is a CPOW.
+ *
+ *        If aArgsOrDocument is an object, that object can take the
+ *        following properties:
+ *
+ *        browser:
+ *          The browser containing the document that we would like to view the
+ *          source of.
+ *        URL:
+ *          A string URL for the page we'd like to view the source of.
+ *        outerWindowID (optional):
+ *          The outerWindowID of the content window containing the document that
+ *          we want to view the source of. You only need to provide this if you
+ *          want to attempt to retrieve the document source from the network
+ *          cache.
+ *        lineNumber (optional):
+ *          The line number to focus on once the source is loaded.
+ */
+function BrowserViewSourceOfDocument(aArgsOrDocument) {
+  let args;
+
+  if (aArgsOrDocument instanceof Document) {
+    let doc = aArgsOrDocument;
+    // Deprecated API - callers should pass args object instead.
+    if (Cu.isCrossProcessWrapper(doc)) {
+      throw new Error("BrowserViewSourceOfDocument cannot accept a CPOW " +
+                      "as a document.");
+    }
+
+    let requestor = doc.defaultView
+                       .QueryInterface(Ci.nsIInterfaceRequestor);
+    let browser = requestor.getInterface(Ci.nsIWebNavigation)
+                           .QueryInterface(Ci.nsIDocShell)
+                           .chromeEventHandler;
+    let outerWindowID = requestor.getInterface(Ci.nsIDOMWindowUtils)
+                                 .outerWindowID;
+    let URL = browser.currentURI.spec;
+    args = { browser, outerWindowID, URL };
+  } else {
+    args = aArgsOrDocument;
+  }
+
+  top.gViewSourceUtils.viewSource(args);
+}
+
+/**
+ * Opens the View Source dialog for the source loaded in the root
+ * top-level document of the browser. This is really just a
+ * convenience wrapper around BrowserViewSourceOfDocument.
+ *
+ * @param browser
+ *        The browser that we want to load the source of.
+ */
+function BrowserViewSource(browser) {
+  BrowserViewSourceOfDocument({
+    browser: browser,
+    outerWindowID: browser.outerWindowID,
+    URL: browser.currentURI.spec,
+  });
 }
 
 // doc - document to use for source, or null for this window's document
 // initialTab - name of the initial tab to display, or null for the first tab
 // imageElement - image to load in the Media Tab of the Page Info window; can be null/omitted
 function BrowserPageInfo(doc, initialTab, imageElement) {
   var args = {doc: doc, initialTab: initialTab, imageElement: imageElement};
   var windows = Services.wm.getEnumerator("Browser:page-info");
--- a/toolkit/components/viewsource/content/viewPartialSource.js
+++ b/toolkit/components/viewsource/content/viewPartialSource.js
@@ -169,24 +169,24 @@ function viewPartialSourceForSelection(s
   // before drawing the selection.
   if (canDrawSelection) {
     window.document.getElementById("content").addEventListener("load", drawSelection, true);
   }
 
   // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
   var loadFlags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE;
   var referrerPolicy = Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT;
-  getWebNavigation().loadURIWithOptions((isHTML ?
-                                         "view-source:data:text/html;charset=utf-8," :
-                                         "view-source:data:application/xml;charset=utf-8,")
-                                        + encodeURIComponent(tmpNode.innerHTML),
-                                        loadFlags,
-                                        null, referrerPolicy,  // referrer
-                                        null, null,  // postData, headers
-                                        Services.io.newURI(doc.baseURI, null, null));
+  ViewSourceChrome.webNav.loadURIWithOptions((isHTML ?
+                                              "view-source:data:text/html;charset=utf-8," :
+                                              "view-source:data:application/xml;charset=utf-8,")
+                                             + encodeURIComponent(tmpNode.innerHTML),
+                                             loadFlags,
+                                             null, referrerPolicy,  // referrer
+                                             null, null,  // postData, headers
+                                             Services.io.newURI(doc.baseURI, null, null));
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // helper to get a path like FIXptr, but with an array instead of the "tumbler" notation
 // see FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
 function getPath(ancestor, node)
 {
   var n = node;
--- a/toolkit/components/viewsource/content/viewPartialSource.xul
+++ b/toolkit/components/viewsource/content/viewPartialSource.xul
@@ -45,19 +45,19 @@
   <command id="cmd_close" oncommand="window.close();"/>
   <commandset id="editMenuCommands"/>
   <command id="cmd_find"
            oncommand="document.getElementById('FindToolbar').onFindCommand();"/>
   <command id="cmd_findAgain"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(false);"/>
   <command id="cmd_findPrevious"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(true);"/>
-  <command id="cmd_goToLine" oncommand="ViewSourceGoToLine();" disabled="true"/>
-  <command id="cmd_highlightSyntax" oncommand="highlightSyntax();"/>
-  <command id="cmd_wrapLongLines" oncommand="wrapLongLines()"/>
+  <command id="cmd_goToLine" oncommand="ViewSourceChrome.promptAndGoToLine();" disabled="true"/>
+  <command id="cmd_highlightSyntax" oncommand="ViewSourceChrome.toggleSyntaxHighlighting();"/>
+  <command id="cmd_wrapLongLines" oncommand="ViewSourceChrome.toggleWrapping();"/>
   <command id="cmd_textZoomReduce" oncommand="ZoomManager.reduce();"/>
   <command id="cmd_textZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
   <command id="cmd_textZoomReset" oncommand="ZoomManager.reset();"/>
 
   <keyset id="editMenuKeys"/>
   <keyset id="viewSourceKeys">
     <key id="key_savePage" key="&savePageCmd.commandkey;" modifiers="accel" command="cmd_savePage"/>
     <key id="key_print" key="&printCmd.commandkey;" modifiers="accel" command="cmd_print"/>
@@ -68,29 +68,28 @@
     <key id="key_textZoomEnlarge2" key="&textEnlarge.commandkey2;" command="cmd_textZoomEnlarge" modifiers="accel"/>
     <key id="key_textZoomEnlarge3" key="&textEnlarge.commandkey3;" command="cmd_textZoomEnlarge" modifiers="accel"/>
     <key id="key_textZoomReduce"  key="&textReduce.commandkey;" command="cmd_textZoomReduce" modifiers="accel"/>
     <key id="key_textZoomReduce2"  key="&textReduce.commandkey2;" command="cmd_textZoomReduce" modifiers="accel"/>
     <key id="key_textZoomReset" key="&textReset.commandkey;" command="cmd_textZoomReset" modifiers="accel"/>
     <key id="key_textZoomReset2" key="&textReset.commandkey2;" command="cmd_textZoomReset" modifiers="accel"/>
   </keyset>
 
-  <menupopup id="viewSourceContextMenu"
-             onpopupshowing="contextMenuShowing();">
+  <menupopup id="viewSourceContextMenu">
     <menuitem id="cMenu_findAgain"/>
     <menuseparator/>
     <menuitem id="cMenu_copy"/>
     <menuitem id="context-copyLink"
               label="&copyLinkCmd.label;"
               accesskey="&copyLinkCmd.accesskey;"
-              oncommand="contextMenuCopyLinkOrEmail();"/>
+              oncommand="ViewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
     <menuitem id="context-copyEmail"
               label="&copyEmailCmd.label;"
               accesskey="&copyEmailCmd.accesskey;"
-              oncommand="contextMenuCopyLinkOrEmail();"/>
+              oncommand="ViewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
     <menuseparator/>
     <menuitem id="cMenu_selectAll"/>
   </menupopup>
 
   <!-- Menu --> 
   <toolbox id="viewSource-toolbox">
     <menubar id="viewSource-main-menubar">
 
@@ -152,13 +151,13 @@
                     label="&menu_highlightSyntax.label;" accesskey="&menu_highlightSyntax.accesskey;"/>
         </menupopup>
       </menu>
     </menubar>  
   </toolbox>
 
   <vbox id="appcontent" flex="1">
     <browser id="content" type="content-primary" name="content" src="about:blank" flex="1"
-             disablehistory="true" context="viewSourceContextMenu"/>
+             disablehistory="true" context="viewSourceContextMenu" />
     <findbar id="FindToolbar" browserid="content"/>
   </vbox>
 
 </window>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource-content.js
@@ -0,0 +1,703 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+  "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+  "resource://gre/modules/DeferredTask.jsm");
+
+const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
+
+let global = this;
+
+/**
+ * ViewSourceContent should be loaded in the <xul:browser> of the
+ * view source window, and initialized as soon as it has loaded.
+ */
+let ViewSourceContent = {
+  /**
+   * We'll act as an nsISelectionListener as well so that we can send
+   * updates to the view source window's status bar.
+   */
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISelectionListener]),
+
+  /**
+   * These are the messages that ViewSourceContent is prepared to listen
+   * for. If you need ViewSourceContent to handle more messages, add them
+   * here.
+   */
+  messages: [
+    "ViewSource:LoadSource",
+    "ViewSource:LoadSourceDeprecated",
+    "ViewSource:GoToLine",
+    "ViewSource:ToggleWrapping",
+    "ViewSource:ToggleSyntaxHighlighting",
+    "ViewSource:SetCharacterSet",
+  ],
+
+  /**
+   * ViewSourceContent is attached as an nsISelectionListener on pageshow,
+   * and removed on pagehide. When the initial about:blank is transitioned
+   * away from, a pagehide is fired without us having attached ourselves
+   * first. We use this boolean to keep track of whether or not we're
+   * attached, so we don't attempt to remove our listener when it's not
+   * yet there (which throws).
+   */
+  selectionListenerAttached: false,
+
+  /**
+   * This should be called as soon as this frame script has loaded.
+   */
+  init() {
+    this.messages.forEach((msgName) => {
+      addMessageListener(msgName, this);
+    });
+
+    addEventListener("pagehide", this, true);
+    addEventListener("pageshow", this, true);
+    addEventListener("click", this);
+    addEventListener("unload", this);
+    Services.els.addSystemEventListener(global, "contextmenu", this, false);
+  },
+
+  /**
+   * This should be called when the frame script is being unloaded,
+   * and the browser is tearing down.
+   */
+  uninit() {
+    this.messages.forEach((msgName) => {
+      removeMessageListener(msgName, this);
+    });
+
+    removeEventListener("pagehide", this, true);
+    removeEventListener("pageshow", this, true);
+    removeEventListener("click", this);
+    removeEventListener("unload", this);
+
+    Services.els.removeSystemEventListener(global, "contextmenu", this, false);
+
+    // Cancel any pending toolbar updates.
+    if (this.updateStatusTask) {
+      this.updateStatusTask.disarm();
+    }
+  },
+
+  /**
+   * Anything added to the messages array will get handled here, and should
+   * get dispatched to a specific function for the message name.
+   */
+  receiveMessage(msg) {
+    let data = msg.data;
+    let objects = msg.objects;
+    switch(msg.name) {
+      case "ViewSource:LoadSource":
+        this.viewSource(data.URL, data.outerWindowID, data.lineNumber,
+                        data.shouldWrap);
+        break;
+      case "ViewSource:LoadSourceDeprecated":
+        this.viewSourceDeprecated(data.URL, objects.pageDescriptor, data.lineNumber,
+                                  data.forcedCharSet);
+        break;
+      case "ViewSource:GoToLine":
+        this.goToLine(data.lineNumber);
+        break;
+      case "ViewSource:ToggleWrapping":
+        this.toggleWrapping();
+        break;
+      case "ViewSource:ToggleSyntaxHighlighting":
+        this.toggleSyntaxHighlighting();
+        break;
+      case "ViewSource:SetCharacterSet":
+        this.setCharacterSet(data.charset, data.doPageLoad);
+        break;
+    }
+  },
+
+  /**
+   * Any events should get handled here, and should get dispatched to
+   * a specific function for the event type.
+   */
+  handleEvent(event) {
+    switch(event.type) {
+      case "pagehide":
+        this.onPageHide(event);
+        break;
+      case "pageshow":
+        this.onPageShow(event);
+        break;
+      case "click":
+        this.onClick(event);
+        break;
+      case "unload":
+        this.uninit();
+        break;
+      case "contextmenu":
+        this.onContextMenu(event);
+        break;
+    }
+  },
+
+  /**
+   * A getter for the view source string bundle.
+   */
+  get bundle() {
+    delete this.bundle;
+    this.bundle = Services.strings.createBundle(BUNDLE_URL);
+    return this.bundle;
+  },
+
+  /**
+   * A shortcut to the nsISelectionController for the content.
+   */
+  get selectionController() {
+    return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsISelectionDisplay)
+                   .QueryInterface(Ci.nsISelectionController);
+  },
+
+  /**
+   * Called when the parent sends a message to view some source code.
+   *
+   * @param URL (required)
+   *        The URL string of the source to be shown.
+   * @param outerWindowID (optional)
+   *        The outerWindowID of the content window that has hosted
+   *        the document, in case we want to retrieve it from the network
+   *        cache.
+   * @param lineNumber (optional)
+   *        The line number to focus as soon as the source has finished
+   *        loading.
+   */
+  viewSource(URL, outerWindowID, lineNumber) {
+    let pageDescriptor, forcedCharSet;
+
+    if (outerWindowID) {
+      let contentWindow = Services.wm.getOuterWindowWithId(outerWindowID);
+      let requestor = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor);
+
+      try {
+        let otherWebNav = requestor.getInterface(Ci.nsIWebNavigation);
+        pageDescriptor = otherWebNav.QueryInterface(Ci.nsIWebPageDescriptor)
+                                    .currentDescriptor;
+      } catch(e) {
+        // We couldn't get the page descriptor, so we'll probably end up re-retrieving
+        // this document off of the network.
+      }
+
+      let utils = requestor.getInterface(Ci.nsIDOMWindowUtils);
+      let doc = contentWindow.document;
+      let forcedCharSet = utils.docCharsetIsForced ? doc.characterSet
+                                                   : null;
+    }
+
+    this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet);
+  },
+
+  /**
+   * Called when the parent is using the deprecated API for viewSource.xul.
+   * This function will throw if it's called on a remote browser.
+   *
+   * @param URL (required)
+   *        The URL string of the source to be shown.
+   * @param pageDescriptor (optional)
+   *        The currentDescriptor off of an nsIWebPageDescriptor, in the
+   *        event that the caller wants to try to load the source out of
+   *        the network cache.
+   * @param lineNumber (optional)
+   *        The line number to focus as soon as the source has finished
+   *        loading.
+   * @param forcedCharSet (optional)
+   *        The document character set to use instead of the default one.
+   */
+  viewSourceDeprecated(URL, pageDescriptor, lineNumber, forcedCharSet) {
+    // This should not be called if this frame script is running
+    // in a content process!
+    if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
+      throw new Error("ViewSource deprecated API should not be used with " +
+                      "remote browsers.");
+    }
+
+    this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet);
+  },
+
+  /**
+   * Common utility function used by both the current and deprecated APIs
+   * for loading source.
+   *
+   * @param URL (required)
+   *        The URL string of the source to be shown.
+   * @param pageDescriptor (optional)
+   *        The currentDescriptor off of an nsIWebPageDescriptor, in the
+   *        event that the caller wants to try to load the source out of
+   *        the network cache.
+   * @param lineNumber (optional)
+   *        The line number to focus as soon as the source has finished
+   *        loading.
+   * @param forcedCharSet (optional)
+   *        The document character set to use instead of the default one.
+   */
+  loadSource(URL, pageDescriptor, lineNumber, forcedCharSet) {
+    const viewSrcURL = "view-source:" + URL;
+    let loadFromURL = false;
+
+    if (forcedCharSet) {
+      docShell.charset = forcedCharSet;
+    }
+
+    if (lineNumber) {
+      let doneLoading = (event) => {
+        this.goToLine(lineNumber);
+        removeEventListener("pageshow", doneLoading);
+      };
+
+      addEventListener("pageshow", doneLoading);
+    }
+
+    if (!pageDescriptor) {
+      this.loadSourceFromURL(viewSrcURL);
+      return;
+    }
+
+    try {
+      let pageLoader = docShell.QueryInterface(Ci.nsIWebPageDescriptor);
+      pageLoader.loadPage(pageDescriptor,
+                          Ci.nsIWebPageDescriptor.DISPLAY_AS_SOURCE);
+    } catch(e) {
+      // We were not able to load the source from the network cache.
+      this.loadSourceFromURL(viewSrcURL);
+      return;
+    }
+
+    let shEntrySource = pageDescriptor.QueryInterface(Ci.nsISHEntry);
+    let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]
+                    .createInstance(Ci.nsISHEntry);
+    shEntry.setURI(BrowserUtils.makeURI(viewSrcURL, null, null));
+    shEntry.setTitle(viewSrcURL);
+    shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
+    shEntry.cacheKey = shEntrySource.cacheKey;
+    docShell.QueryInterface(Ci.nsIWebNavigation)
+            .sessionHistory
+            .QueryInterface(Ci.nsISHistoryInternal)
+            .addEntry(shEntry, true);
+  },
+
+  /**
+   * Load some URL in the browser.
+   *
+   * @param URL
+   *        The URL string to load.
+   */
+  loadSourceFromURL(URL) {
+    let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+    let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+    webNav.loadURI(URL, loadFlags, null, null, null);
+  },
+
+  /**
+   * This handler is specifically for click events bubbling up from
+   * error page content, which can show up if the user attempts to
+   * view the source of an attack page.
+   */
+  onClick(event) {
+    // Don't trust synthetic events
+    if (!event.isTrusted || event.target.localName != "button")
+      return;
+
+    let target = event.originalTarget;
+    let errorDoc = target.ownerDocument;
+
+    if (/^about:blocked/.test(errorDoc.documentURI)) {
+      // The event came from a button on a malware/phishing block page
+
+      if (target == errorDoc.getElementById("getMeOutButton")) {
+        // Instead of loading some safe page, just close the window
+        sendAsyncMessage("ViewSource:Close");
+      } else if (target == errorDoc.getElementById("reportButton")) {
+        // This is the "Why is this site blocked" button. We redirect
+        // to the generic page describing phishing/malware protection.
+        let URL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+        sendAsyncMessage("ViewSource:OpenURL", { URL })
+      } else if (target == errorDoc.getElementById("ignoreWarningButton")) {
+        // Allow users to override and continue through to the site
+        docShell.QueryInterface(Ci.nsIWebNavigation)
+                .loadURIWithOptions(content.location.href,
+                                    Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
+                                    null, Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
+                                    null, null, null);
+      }
+    }
+  },
+
+  /**
+   * Handler for the pageshow event.
+   *
+   * @param event
+   *        The pageshow event being handled.
+   */
+  onPageShow(event) {
+    content.getSelection()
+           .QueryInterface(Ci.nsISelectionPrivate)
+           .addSelectionListener(this);
+    this.selectionListenerAttached = true;
+
+    content.focus();
+    sendAsyncMessage("ViewSource:SourceLoaded");
+  },
+
+  /**
+   * Handler for the pagehide event.
+   *
+   * @param event
+   *        The pagehide event being handled.
+   */
+  onPageHide(event) {
+    // The initial about:blank will fire pagehide before we
+    // ever set a selectionListener, so we have a boolean around
+    // to keep track of when the listener is attached.
+    if (this.selectionListenerAttached) {
+      content.getSelection()
+             .QueryInterface(Ci.nsISelectionPrivate)
+             .removeSelectionListener(this);
+      this.selectionListenerAttached = false;
+    }
+    sendAsyncMessage("ViewSource:SourceUnloaded");
+  },
+
+  onContextMenu(event) {
+    let addonInfo = {};
+    let subject = {
+      event: event,
+      addonInfo: addonInfo,
+    };
+
+    subject.wrappedJSObject = subject;
+    Services.obs.notifyObservers(subject, "content-contextmenu", null);
+
+    let node = event.target;
+
+    let result = {
+      isEmail: false,
+      isLink: false,
+      href: "",
+      // We have to pass these in the event that we're running in
+      // a remote browser, so that ViewSourceChrome knows where to
+      // open the context menu.
+      screenX: event.screenX,
+      screenY: event.screenY,
+    };
+
+    if (node && node.localName == "a") {
+      result.isLink = node.href.startsWith("view-source:");
+      result.isEmail = node.href.startsWith("mailto:");
+      result.href = node.href.substring(node.href.indexOf(":") + 1);
+    }
+
+    sendSyncMessage("ViewSource:ContextMenuOpening", result);
+  },
+
+  /**
+   * Attempts to go to a particular line in the source code being
+   * shown. If it succeeds in finding the line, it will fire a
+   * "ViewSource:GoToLine:Success" message, passing up an object
+   * with the lineNumber we just went to. If it cannot find the line,
+   * it will fire a "ViewSource:GoToLine:Failed" message.
+   *
+   * @param lineNumber
+   *        The line number to attempt to go to.
+   */
+  goToLine(lineNumber) {
+    let body = content.document.body;
+
+    // The source document is made up of a number of pre elements with
+    // id attributes in the format <pre id="line123">, meaning that
+    // the first line in the pre element is number 123.
+    // Do binary search to find the pre element containing the line.
+    // However, in the plain text case, we have only one pre without an
+    // attribute, so assume it begins on line 1.
+    let pre;
+    for (let lbound = 0, ubound = body.childNodes.length; ; ) {
+      let middle = (lbound + ubound) >> 1;
+      pre = body.childNodes[middle];
+
+      let firstLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
+
+      if (lbound == ubound - 1) {
+        break;
+      }
+
+      if (lineNumber >= firstLine) {
+        lbound = middle;
+      } else {
+        ubound = middle;
+      }
+    }
+
+    let result = {};
+    let found = this.findLocation(pre, lineNumber, null, -1, false, result);
+
+    if (!found) {
+      sendAsyncMessage("ViewSource:GoToLine:Failed");
+      return;
+    }
+
+    let selection = content.getSelection();
+    selection.removeAllRanges();
+
+    // In our case, the range's startOffset is after "\n" on the previous line.
+    // Tune the selection at the beginning of the next line and do some tweaking
+    // to position the focusNode and the caret at the beginning of the line.
+    selection.QueryInterface(Ci.nsISelectionPrivate)
+      .interlinePosition = true;
+
+    selection.addRange(result.range);
+
+    if (!selection.isCollapsed) {
+      selection.collapseToEnd();
+
+      let offset = result.range.startOffset;
+      let node = result.range.startContainer;
+      if (offset < node.data.length) {
+        // The same text node spans across the "\n", just focus where we were.
+        selection.extend(node, offset);
+      }
+      else {
+        // There is another tag just after the "\n", hook there. We need
+        // to focus a safe point because there are edgy cases such as
+        // <span>...\n</span><span>...</span> vs.
+        // <span>...\n<span>...</span></span><span>...</span>
+        node = node.nextSibling ? node.nextSibling : node.parentNode.nextSibling;
+        selection.extend(node, 0);
+      }
+    }
+
+    let selCon = this.selectionController;
+    selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
+    selCon.setCaretVisibilityDuringSelection(true);
+
+    // Scroll the beginning of the line into view.
+    selCon.scrollSelectionIntoView(
+      Ci.nsISelectionController.SELECTION_NORMAL,
+      Ci.nsISelectionController.SELECTION_FOCUS_REGION,
+      true);
+
+    sendAsyncMessage("ViewSource:GoToLine:Success", { lineNumber });
+  },
+
+
+  /**
+   * Some old code from the original view source implementation. Original
+   * documentation follows:
+   *
+   * "Loops through the text lines in the pre element. The arguments are either
+   *  (pre, line) or (node, offset, interlinePosition). result is an out
+   *  argument. If (pre, line) are specified (and node == null), result.range is
+   *  a range spanning the specified line. If the (node, offset,
+   *  interlinePosition) are specified, result.line and result.col are the line
+   *  and column number of the specified offset in the specified node relative to
+   *  the whole file."
+   */
+  findLocation(pre, lineNumber, node, offset, interlinePosition, result) {
+    if (node && !pre) {
+      // Look upwards to find the current pre element.
+      for (pre = node;
+           pre.nodeName != "PRE";
+           pre = pre.parentNode);
+    }
+
+    // The source document is made up of a number of pre elements with
+    // id attributes in the format <pre id="line123">, meaning that
+    // the first line in the pre element is number 123.
+    // However, in the plain text case, there is only one <pre> without an id,
+    // so assume line 1.
+    let curLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
+
+    // Walk through each of the text nodes and count newlines.
+    let treewalker = content.document
+        .createTreeWalker(pre, Ci.nsIDOMNodeFilter.SHOW_TEXT, null);
+
+    // The column number of the first character in the current text node.
+    let firstCol = 1;
+
+    let found = false;
+    for (let textNode = treewalker.firstChild();
+         textNode && !found;
+         textNode = treewalker.nextNode()) {
+
+      // \r is not a valid character in the DOM, so we only check for \n.
+      let lineArray = textNode.data.split(/\n/);
+      let lastLineInNode = curLine + lineArray.length - 1;
+
+      // Check if we can skip the text node without further inspection.
+      if (node ? (textNode != node) : (lastLineInNode < lineNumber)) {
+        if (lineArray.length > 1) {
+          firstCol = 1;
+        }
+        firstCol += lineArray[lineArray.length - 1].length;
+        curLine = lastLineInNode;
+        continue;
+      }
+
+      // curPos is the offset within the current text node of the first
+      // character in the current line.
+      for (var i = 0, curPos = 0;
+           i < lineArray.length;
+           curPos += lineArray[i++].length + 1) {
+
+        if (i > 0) {
+          curLine++;
+        }
+
+        if (node) {
+          if (offset >= curPos && offset <= curPos + lineArray[i].length) {
+            // If we are right after the \n of a line and interlinePosition is
+            // false, the caret looks as if it were at the end of the previous
+            // line, so we display that line and column instead.
+
+            if (i > 0 && offset == curPos && !interlinePosition) {
+              result.line = curLine - 1;
+              var prevPos = curPos - lineArray[i - 1].length;
+              result.col = (i == 1 ? firstCol : 1) + offset - prevPos;
+            } else {
+              result.line = curLine;
+              result.col = (i == 0 ? firstCol : 1) + offset - curPos;
+            }
+            found = true;
+
+            break;
+          }
+
+        } else {
+          if (curLine == lineNumber && !("range" in result)) {
+            result.range = content.document.createRange();
+            result.range.setStart(textNode, curPos);
+
+            // This will always be overridden later, except when we look for
+            // the very last line in the file (this is the only line that does
+            // not end with \n).
+            result.range.setEndAfter(pre.lastChild);
+
+          } else if (curLine == lineNumber + 1) {
+            result.range.setEnd(textNode, curPos - 1);
+            found = true;
+            break;
+          }
+        }
+      }
+    }
+
+    return found || ("range" in result);
+  },
+
+  /**
+   * Toggles the "wrap" class on the document body, which sets whether
+   * or not long lines are wrapped.
+   */
+  toggleWrapping() {
+    let body = content.document.body;
+    body.classList.toggle("wrap");
+  },
+
+  /**
+   * Called when the parent has changed the syntax highlighting pref.
+   */
+  toggleSyntaxHighlighting() {
+    // The parent process should have set the view_source.syntax_highlight
+    // pref to the desired value. The reload brings that setting into
+    // effect.
+    this.reload();
+  },
+
+  /**
+   * Called when the parent has changed the character set to view the
+   * source with.
+   *
+   * @param charset
+   *        The character set to use.
+   * @param doPageLoad
+   *        Whether or not we should reload the page ourselves with the
+   *        nsIWebPageDescriptor. Part of a workaround for bug 136322.
+   */
+  setCharacterSet(charset, doPageLoad) {
+    docShell.charset = charset;
+    if (doPageLoad) {
+      this.reload();
+    }
+  },
+
+  /**
+   * Reloads the content.
+   */
+  reload() {
+    let pageLoader = docShell.QueryInterface(Ci.nsIWebPageDescriptor);
+    try {
+      pageLoader.loadPage(pageLoader.currentDescriptor,
+                          Ci.nsIWebPageDescriptor.DISPLAY_NORMAL);
+    } catch(e) {
+      let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+      webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
+    }
+  },
+
+  /**
+   * A reference to a DeferredTask that is armed every time the
+   * selection changes.
+   */
+  updateStatusTask: null,
+
+  /**
+   * Called once the DeferredTask fires. Sends a message up to the
+   * parent to update the status bar text.
+   */
+  updateStatus() {
+    let selection = content.getSelection();
+
+    if (!selection.focusNode) {
+      sendAsyncMessage("ViewSource:UpdateStatus", { label: "" });
+      return;
+    }
+    if (selection.focusNode.nodeType != Ci.nsIDOMNode.TEXT_NODE) {
+      return;
+    }
+
+    let selCon = this.selectionController;
+    selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
+    selCon.setCaretVisibilityDuringSelection(true);
+
+    let interlinePosition = selection.QueryInterface(Ci.nsISelectionPrivate)
+                                     .interlinePosition;
+
+    let result = {};
+    this.findLocation(null, -1,
+        selection.focusNode, selection.focusOffset, interlinePosition, result);
+
+    let label = this.bundle.formatStringFromName("statusBarLineCol",
+                                                 [result.line, result.col], 2);
+    sendAsyncMessage("ViewSource:UpdateStatus", { label });
+  },
+
+  /**
+   * nsISelectionListener
+   */
+
+  /**
+   * Gets called every time the selection is changed. Coalesces frequent
+   * changes, and calls updateStatus after 100ms of no selection change
+   * activity.
+   */
+  notifySelectionChanged(doc, sel, reason) {
+    if (!this.updateStatusTask) {
+      this.updateStatusTask = new DeferredTask(() => {
+        this.updateStatus();
+      }, 100);
+    }
+
+    this.updateStatusTask.arm();
+  },
+};
+ViewSourceContent.init();
--- a/toolkit/components/viewsource/content/viewSource.css
+++ b/toolkit/components/viewsource/content/viewSource.css
@@ -1,7 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 toolbar[printpreview="true"] {
   -moz-binding: url("chrome://global/content/printPreviewBindings.xml#printpreviewtoolbar");
 }
+
+browser[remote="true"] {
+  -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser");
+}
\ No newline at end of file
--- a/toolkit/components/viewsource/content/viewSource.js
+++ b/toolkit/components/viewsource/content/viewSource.js
@@ -1,703 +1,879 @@
 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-Components.utils.import("resource://gre/modules/Services.jsm");
-Components.utils.import("resource://gre/modules/CharsetMenu.jsm");
+const { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-
-var gLastLineFound = '';
-var gGoToLine = 0;
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
+  "resource://gre/modules/CharsetMenu.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+  "resource://gre/modules/Deprecated.jsm");
 
 [
   ["gBrowser",          "content"],
   ["gViewSourceBundle", "viewSourceBundle"],
   ["gContextMenu",      "viewSourceContextMenu"]
 ].forEach(function ([name, id]) {
   window.__defineGetter__(name, function () {
     var element = document.getElementById(id);
     if (!element)
       return null;
     delete window[name];
     return window[name] = element;
   });
 });
 
-// viewZoomOverlay.js uses this
-function getBrowser() {
-  return gBrowser;
-}
+/**
+ * ViewSourceChrome is the primary interface for interacting with
+ * the view source browser. It initializes itself on script load.
+ */
+let ViewSourceChrome = {
+  /**
+   * Holds the value of the last line found via the "Go to line"
+   * command, to pre-populate the prompt the next time it is
+   * opened.
+   */
+  lastLineFound: null,
 
-this.__defineGetter__("gPageLoader", function () {
-  var webnav = getWebNavigation();
-  if (!webnav)
-    return null;
-  delete this.gPageLoader;
-  return this.gPageLoader = webnav.QueryInterface(Ci.nsIWebPageDescriptor);
-});
+  /**
+   * The context menu, when opened from the content process, sends
+   * up a chunk of serialized data describing the items that the
+   * context menu is being opened on. This allows us to avoid using
+   * CPOWs.
+   */
+  contextMenuData: {},
 
-var gSelectionListener = {
-  timeout: 0,
-  attached: false,
-  notifySelectionChanged: function(doc, sel, reason)
-  {
-    // Coalesce notifications within 100ms intervals.
-    if (!this.timeout)
-      this.timeout = setTimeout(updateStatusBar, 100);
-  }
-}
+  /**
+   * These are the messages that ViewSourceChrome will listen for
+   * from the frame script it injects. Any message names added here
+   * will automatically have ViewSourceChrome listen for those messages,
+   * and remove the listeners on teardown.
+   */
+  messages: [
+    "ViewSource:SourceLoaded",
+    "ViewSource:SourceUnloaded",
+    "ViewSource:Close",
+    "ViewSource:OpenURL",
+    "ViewSource:GoToLine:Success",
+    "ViewSource:GoToLine:Failed",
+    "ViewSource:UpdateStatus",
+    "ViewSource:ContextMenuOpening",
+  ],
 
-function onLoadViewSource() 
-{
-  viewSource(window.arguments[0]);
-  document.commandDispatcher.focusedWindow = content;
-  gBrowser.droppedLinkHandler = function (event, url, name) {
-    viewSource(url)
-    event.preventDefault();
-  }
+  /**
+   * This should be called as soon as the script loads. When this function
+   * executes, we can assume the DOM content has not yet loaded.
+   */
+  init() {
+    // We use the window message manager so that if we switch remoteness of the
+    // browser (which we might do if we're attempting to load the document
+    // source out of the network cache), we automatically re-load the frame
+    // script.
+    let wMM = window.messageManager;
+    wMM.loadFrameScript("chrome://global/content/viewSource-content.js", true);
+    this.messages.forEach((msgName) => {
+      wMM.addMessageListener(msgName, this);
+    });
+
+    this.shouldWrap = Services.prefs.getBoolPref("view_source.wrap_long_lines");
+    this.shouldHighlight =
+      Services.prefs.getBoolPref("view_source.syntax_highlight");
+
+    addEventListener("load", this);
+    addEventListener("unload", this);
+    addEventListener("AppCommand", this, true);
+    addEventListener("MozSwipeGesture", this, true);
+  },
 
-  if (!isHistoryEnabled()) {
-    // Disable the BACK and FORWARD commands and hide the related menu items.
-    var viewSourceNavigation = document.getElementById("viewSourceNavigation");
-    viewSourceNavigation.setAttribute("disabled", "true");
-    viewSourceNavigation.setAttribute("hidden", "true");
-  }
-}
+  /**
+   * This should be called when the window is closing. This function should
+   * clean up event and message listeners.
+   */
+  uninit() {
+    let wMM = window.messageManager;
+    this.messages.forEach((msgName) => {
+      wMM.removeMessageListener(msgName, this);
+    });
 
-function isHistoryEnabled() {
-  return !gBrowser.hasAttribute("disablehistory");
-}
+    // "load" event listener is removed in its handler, to
+    // ensure we only fire it once.
+    removeEventListener("unload", this);
+    removeEventListener("AppCommand", this, true);
+    removeEventListener("MozSwipeGesture", this, true);
+    gContextMenu.removeEventListener("popupshowing", this);
+    gContextMenu.removeEventListener("popuphidden", this);
+  },
 
-function getSelectionController() {
-  return gBrowser.docShell
-                 .QueryInterface(Ci.nsIInterfaceRequestor)
-                 .getInterface(Ci.nsISelectionDisplay)
-                 .QueryInterface(Ci.nsISelectionController);
-}
+  /**
+   * Anything added to the messages array will get handled here, and should
+   * get dispatched to a specific function for the message name.
+   */
+  receiveMessage(message) {
+    let data = message.data;
 
-function viewSource(url)
-{
-  if (!url)
-    return; // throw Components.results.NS_ERROR_FAILURE;
-    
-  var viewSrcUrl = "view-source:" + url;
-
-  gBrowser.addEventListener("pagehide", onUnloadContent, true);
-  gBrowser.addEventListener("pageshow", onLoadContent, true);
-  gBrowser.addEventListener("click", onClickContent, false);
+    switch(message.name) {
+      case "ViewSource:SourceLoaded":
+        this.onSourceLoaded();
+        break;
+      case "ViewSource:SourceUnloaded":
+        this.onSourceUnloaded();
+        break;
+      case "ViewSource:Close":
+        this.close();
+        break;
+      case "ViewSource:OpenURL":
+        this.openURL(data.URL);
+        break;
+      case "ViewSource:GoToLine:Failed":
+        this.onGoToLineFailed();
+        break;
+      case "ViewSource:GoToLine:Success":
+        this.onGoToLineSuccess(data.lineNumber);
+        break;
+      case "ViewSource:UpdateStatus":
+        this.updateStatus(data.label);
+        break;
+      case "ViewSource:ContextMenuOpening":
+        this.onContextMenuOpening(data.isLink, data.isEmail, data.href);
+        if (gBrowser.isRemoteBrowser) {
+          this.openContextMenu(data.screenX, data.screenY);
+        }
+        break;
+    }
+  },
 
-  var loadFromURL = true;
+  /**
+   * Any events should get handled here, and should get dispatched to
+   * a specific function for the event type.
+   */
+  handleEvent(event) {
+    switch(event.type) {
+      case "unload":
+        this.uninit();
+        break;
+      case "load":
+        this.onXULLoaded();
+        break;
+      case "AppCommand":
+        this.onAppCommand(event);
+        break;
+      case "MozSwipeGesture":
+        this.onSwipeGesture(event);
+        break;
+      case "popupshowing":
+        this.onContextMenuShowing(event);
+        break;
+      case "popuphidden":
+        this.onContextMenuHidden(event);
+        break;
+    }
+  },
 
-  // Parse the 'arguments' supplied with the dialog.
-  //    arg[0] - URL string.
-  //    arg[1] - Charset value in the form 'charset=xxx'.
-  //    arg[2] - Page descriptor used to load content from the cache.
-  //    arg[3] - Line number to go to.
-  //    arg[4] - Whether charset was forced by the user
+  /**
+   * Getter that returns whether or not the view source browser
+   * has history enabled on it.
+   */
+  get historyEnabled() {
+    return !gBrowser.hasAttribute("disablehistory");
+  },
 
-  if ("arguments" in window) {
-    var arg;
+  /**
+   * Getter for the message manager of the view source browser.
+   */
+  get mm() {
+    return gBrowser.messageManager;
+  },
 
-    // Set the charset of the viewsource window...
-    var charset;
-    if (window.arguments.length >= 2) {
-      arg = window.arguments[1];
+  /**
+   * Getter for the nsIWebNavigation of the view source browser.
+   */
+  get webNav() {
+    return gBrowser.webNavigation;
+  },
+
+  /**
+   * Send the browser forward in its history.
+   */
+  goForward() {
+    gBrowser.goForward();
+  },
+
+  /**
+   * Send the browser backward in its history.
+   */
+  goBack() {
+    gBrowser.goBack();
+  },
 
-      try {
-        if (typeof(arg) == "string" && arg.indexOf('charset=') != -1) {
-          var arrayArgComponents = arg.split('=');
-          if (arrayArgComponents) {
-            // Remember the charset here so that it can be used below in case
-            // the document had a forced charset.
-            charset = arrayArgComponents[1];
-          }
-        }
-      } catch (ex) {
-        // Ignore the failure and keep processing arguments...
-      }
+  /**
+   * This should be called once when the DOM has finished loading. Here we
+   * set the state of various menu items, and add event listeners to
+   * DOM nodes.
+   *
+   * This is also the place where we handle any arguments that have been
+   * passed to viewSource.xul.
+   *
+   * Modern consumers should pass a single object argument to viewSource.xul:
+   *
+   *   URL (required):
+   *     A string URL for the page we'd like to view the source of.
+   *   browser:
+   *     The browser containing the document that we would like to view the
+   *     source of. This argument is optional if outerWindowID is not passed.
+   *   outerWindowID (optional):
+   *     The outerWindowID of the content window containing the document that
+   *     we want to view the source of. This is the only way of attempting to
+   *     load the source out of the network cache.
+   *   lineNumber (optional):
+   *     The line number to focus on once the source is loaded.
+   *
+   * The deprecated API has the opener pass in a number of arguments:
+   *
+   * arg[0] - URL string.
+   * arg[1] - Charset value string in the form 'charset=xxx'.
+   * arg[2] - Page descriptor from nsIWebPageDescriptor used to load content
+   *          from the cache.
+   * arg[3] - Line number to go to.
+   * arg[4] - Boolean for whether charset was forced by the user
+   */
+  onXULLoaded() {
+    // This handler should only ever run the first time the XUL is loaded.
+    removeEventListener("load", this);
+
+    let wrapMenuItem = document.getElementById("menu_wrapLongLines");
+    if (this.shouldWrap) {
+      wrapMenuItem.setAttribute("checked", "true");
     }
-    // If the document had a forced charset, set it here also
-    if (window.arguments.length >= 5) {
-      arg = window.arguments[4];
+
+    let highlightMenuItem = document.getElementById("menu_highlightSyntax");
+    if (this.shouldHighlight) {
+      highlightMenuItem.setAttribute("checked", "true");
+    }
 
-      try {
-        if (arg === true) {
-          gBrowser.docShell.charset = charset;
-        }
-      } catch (ex) {
-        // Ignore the failure and keep processing arguments...
+    gContextMenu.addEventListener("popupshowing", this);
+    gContextMenu.addEventListener("popuphidden", this);
+
+    if (!this.historyEnabled) {
+      // Disable the BACK and FORWARD commands and hide the related menu items.
+      let viewSourceNavigation = document.getElementById("viewSourceNavigation");
+      if (viewSourceNavigation) {
+        viewSourceNavigation.setAttribute("disabled", "true");
+        viewSourceNavigation.setAttribute("hidden", "true");
       }
     }
 
-    // Get any specified line to jump to.
-    if (window.arguments.length >= 4) {
-      arg = window.arguments[3];
-      gGoToLine = parseInt(arg);
+    // This will only work with non-remote browsers. See bug 1158377.
+    gBrowser.droppedLinkHandler = function (event, url, name) {
+      ViewSourceChrome.loadURL(url);
+      event.preventDefault();
+    };
+
+    // We require the first argument to do any loading of source.
+    // otherwise, we're done.
+    if (!window.arguments[0]) {
+      return;
+    }
+
+    if (typeof window.arguments[0] == "string") {
+      // We're using the deprecated API
+      return ViewSourceChrome._loadViewSourceDeprecated();
+    }
+
+    // We're using the modern API, which allows us to view the
+    // source of documents from out of process browsers.
+    let args = window.arguments[0];
+
+    if (!args.URL) {
+      throw new Error("Must supply a URL when opening view source.");
     }
 
-    // Use the page descriptor to load the content from the cache (if
-    // available).
-    if (window.arguments.length >= 3) {
-      arg = window.arguments[2];
+    if (args.browser) {
+      // If we're dealing with a remote browser, then the browser
+      // for view source needs to be remote as well.
+      this.updateBrowserRemoteness(args.browser.isRemoteBrowser);
+    } else {
+      if (args.outerWindowID) {
+        throw new Error("Must supply the browser if passing the outerWindowID");
+      }
+    }
 
-      try {
-        if (typeof(arg) == "object" && arg != null) {
-          // Load the page using the page descriptor rather than the URL.
-          // This allows the content to be fetched from the cache (if
-          // possible) rather than the network...
-          gPageLoader.loadPage(arg, gPageLoader.DISPLAY_AS_SOURCE);
-
-          // The content was successfully loaded.
-          loadFromURL = false;
+    this.mm.sendAsyncMessage("ViewSource:LoadSource", {
+      URL: args.URL,
+      outerWindowID: args.outerWindowID,
+      lineNumber: args.lineNumber,
+    });
+  },
 
-          // Record the page load in the session history so <back> will work.
-          var shEntrySource = arg.QueryInterface(Ci.nsISHEntry);
-          var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry);
-          shEntry.setURI(makeURI(viewSrcUrl, null, null));
-          shEntry.setTitle(viewSrcUrl);
-          shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
-          shEntry.cacheKey = shEntrySource.cacheKey;
-          gBrowser.sessionHistory
-                  .QueryInterface(Ci.nsISHistoryInternal)
-                  .addEntry(shEntry, true);
-        }
-      } catch(ex) {
-        // Ignore the failure.  The content will be loaded via the URL
-        // that was supplied in arg[0].
+  /**
+   * This is the deprecated API for viewSource.xul, for old-timer consumers.
+   * This API might eventually go away.
+   */
+  _loadViewSourceDeprecated() {
+    Deprecated.warning("The arguments you're passing to viewSource.xul " +
+                       "are using an out-of-date API.",
+                       "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+    // Parse the 'arguments' supplied with the dialog.
+    //    arg[0] - URL string.
+    //    arg[1] - Charset value in the form 'charset=xxx'.
+    //    arg[2] - Page descriptor used to load content from the cache.
+    //    arg[3] - Line number to go to.
+    //    arg[4] - Whether charset was forced by the user
+
+    if (window.arguments[3] == "selection" ||
+        window.arguments[3] == "mathml") {
+      // viewPartialSource.js will take care of loading the content.
+      return;
+    }
+
+    if (window.arguments[2]) {
+      let pageDescriptor = window.arguments[2];
+      if (Cu.isCrossProcessWrapper(pageDescriptor)) {
+        throw new Error("Cannot pass a CPOW as the page descriptor to viewSource.xul.");
       }
     }
-  }
 
-  if (loadFromURL) {
-    // Currently, an exception is thrown if the URL load fails...
-    var loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
-    getWebNavigation().loadURI(viewSrcUrl, loadFlags, null, null, null);
-  }
-
-  // Check the view_source.wrap_long_lines pref and set the menuitem's checked
-  // attribute accordingly.
-  var wraplonglinesPrefValue = Services.prefs.getBoolPref("view_source.wrap_long_lines");
-
-  if (wraplonglinesPrefValue)
-    document.getElementById("menu_wrapLongLines").setAttribute("checked", "true");
-
-  document.getElementById("menu_highlightSyntax")
-          .setAttribute("checked",
-                        Services.prefs.getBoolPref("view_source.syntax_highlight"));
-
-  window.addEventListener("AppCommand", HandleAppCommandEvent, true);
-  window.addEventListener("MozSwipeGesture", HandleSwipeGesture, true);
-  window.content.focus();
-}
-
-function onLoadContent()
-{
-  // If the view source was opened with a "go to line" argument.
-  if (gGoToLine > 0) {
-    goToLine(gGoToLine);
-    gGoToLine = 0;
-  }
-  document.getElementById('cmd_goToLine').removeAttribute('disabled');
+    if (gBrowser.isRemoteBrowser) {
+      throw new Error("Deprecated view source API should not use a remote browser.");
+    }
 
-  // Register a listener so that we can show the caret position on the status bar.
-  window.content.getSelection()
-   .QueryInterface(Ci.nsISelectionPrivate)
-   .addSelectionListener(gSelectionListener);
-  gSelectionListener.attached = true;
-
-  if (isHistoryEnabled())
-    UpdateBackForwardCommands();
-}
-
-function onUnloadContent()
-{
-  // Disable "go to line" while reloading due to e.g. change of charset
-  // or toggling of syntax highlighting.
-  document.getElementById('cmd_goToLine').setAttribute('disabled', 'true');
+    let forcedCharSet;
+    if (window.arguments[4] && window.arguments[1].startsWith("charset=")) {
+      forcedCharSet = window.arguments[1].split("=")[1];
+    }
 
-  // If we're not just unloading the initial "about:blank" which doesn't have
-  // a selection listener, get rid of it so it doesn't try to fire after the
-  // window has gone away.
-  if (gSelectionListener.attached) {
-    window.content.getSelection().QueryInterface(Ci.nsISelectionPrivate)
-          .removeSelectionListener(gSelectionListener);
-    gSelectionListener.attached = false;
-  }
-}
-
-/**
- * Handle click events bubbling up from error page content
- */
-function onClickContent(event) {
-  // Don't trust synthetic events
-  if (!event.isTrusted || event.target.localName != "button")
-    return;
-
-  var target = event.originalTarget;
-  var errorDoc = target.ownerDocument;
-
-  if (/^about:blocked/.test(errorDoc.documentURI)) {
-    // The event came from a button on a malware/phishing block page
+    gBrowser.messageManager.sendAsyncMessage("ViewSource:LoadSourceDeprecated", {
+      URL: window.arguments[0],
+      lineNumber: window.arguments[3],
+      forcedCharSet,
+    }, {
+      pageDescriptor: window.arguments[2],
+    });
+  },
 
-    if (target == errorDoc.getElementById('getMeOutButton')) {
-      // Instead of loading some safe page, just close the window
-      window.close();
-    } else if (target == errorDoc.getElementById('reportButton')) {
-      // This is the "Why is this site blocked" button. We redirect
-      // to the generic page describing phishing/malware protection.
-      let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
-      openURL(url + "phishing-malware");
-    } else if (target == errorDoc.getElementById('ignoreWarningButton')) {
-      // Allow users to override and continue through to the site
-      gBrowser.loadURIWithFlags(content.location.href,
-                                Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
-                                null, null, null);
+  /**
+   * Handler for the AppCommand event.
+   *
+   * @param event
+   *        The AppCommand event being handled.
+   */
+  onAppCommand(event) {
+    event.stopPropagation();
+    switch (event.command) {
+      case "Back":
+        this.goBack();
+        break;
+      case "Forward":
+        this.goForward();
+        break;
     }
-  }
-}
-
-function HandleAppCommandEvent(evt)
-{
-  evt.stopPropagation();
-  switch (evt.command) {
-    case "Back":
-      BrowserBack();
-      break;
-    case "Forward":
-      BrowserForward();
-      break;
-  }
-}
+  },
 
-function HandleSwipeGesture(evt) {
-  evt.stopPropagation();
-  switch (evt.direction) {
-    case SimpleGestureEvent.DIRECTION_LEFT:
-      BrowserBack();
-      break;
-    case SimpleGestureEvent.DIRECTION_RIGHT:
-      BrowserForward();
-      break;
-    case SimpleGestureEvent.DIRECTION_UP:
-      goDoCommand("cmd_scrollTop");
-      break;
-    case SimpleGestureEvent.DIRECTION_DOWN:
-      goDoCommand("cmd_scrollBottom");
-      break;
-  }
-}
-
-function ViewSourceClose()
-{
-  window.close();
-}
+  /**
+   * Handler for the MozSwipeGesture event.
+   *
+   * @param event
+   *        The MozSwipeGesture event being handled.
+   */
+  onSwipeGesture(event) {
+    event.stopPropagation();
+    switch (event.direction) {
+      case SimpleGestureEvent.DIRECTION_LEFT:
+        this.goBack();
+        break;
+      case SimpleGestureEvent.DIRECTION_RIGHT:
+        this.goForward();
+        break;
+      case SimpleGestureEvent.DIRECTION_UP:
+        goDoCommand("cmd_scrollTop");
+        break;
+      case SimpleGestureEvent.DIRECTION_DOWN:
+        goDoCommand("cmd_scrollBottom");
+        break;
+    }
+  },
 
-function ViewSourceReload()
-{
-  gBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
-                           Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
-}
+  /**
+   * Called as soon as the frame script reports that some source
+   * code has been loaded in the browser.
+   */
+  onSourceLoaded() {
+    document.getElementById("cmd_goToLine").removeAttribute("disabled");
 
-// Strips the |view-source:| for internalSave()
-function ViewSourceSavePage()
-{
-  internalSave(window.content.location.href.replace(/^view-source:/i, ""),
-               null, null, null, null, null, "SaveLinkTitle",
-               null, null, window.content.document, null, gPageLoader);
-}
-
-var PrintPreviewListener = {
-  getPrintPreviewBrowser: function () {
-    var browser = document.getElementById("ppBrowser");
-    if (!browser) {
-      browser = document.createElement("browser");
-      browser.setAttribute("id", "ppBrowser");
-      browser.setAttribute("flex", "1");
-      browser.setAttribute("type", "content");
-      document.getElementById("appcontent").
-        insertBefore(browser, document.getElementById("FindToolbar"));
+    if (this.historyEnabled) {
+      this.updateCommands();
     }
 
-    return browser;
+    gBrowser.focus();
+  },
+
+  /**
+   * Called as soon as the frame script reports that some source
+   * code has been unloaded from the browser.
+   */
+  onSourceUnloaded() {
+    // Disable "go to line" while reloading due to e.g. change of charset
+    // or toggling of syntax highlighting.
+    document.getElementById("cmd_goToLine").setAttribute("disabled", "true");
   },
-  getSourceBrowser: function () {
-    return gBrowser;
+
+  /**
+   * Called by clicks on a menu populated by CharsetMenu.jsm to
+   * change the selected character set.
+   *
+   * @param event
+   *        The click event on a character set menuitem.
+   */
+  onSetCharacterSet(event) {
+    if (event.target.hasAttribute("charset")) {
+      let charset = event.target.getAttribute("charset");
+
+      // If we don't have history enabled, we have to do a reload in order to
+      // show the character set change. See bug 136322.
+      this.mm.sendAsyncMessage("ViewSource:SetCharacterSet", {
+        charset: charset,
+        doPageLoad: this.historyEnabled,
+      });
+
+      if (this.historyEnabled) {
+        gBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+      }
+    }
   },
-  getNavToolbox: function () {
-    return document.getElementById("appcontent");
+
+  /**
+   * Called from the frame script when the context menu is about to
+   * open. This tells ViewSourceChrome things about the item that
+   * the context menu is being opened on. This should be called before
+   * the popupshowing event handler fires.
+   */
+  onContextMenuOpening(isLink, isEmail, href) {
+    this.contextMenuData = { isLink, isEmail, href, isOpen: true };
   },
-  onEnter: function () {
-    var toolbox = document.getElementById("viewSource-toolbox");
-    toolbox.hidden = true;
-    gBrowser.collapsed = true;
+
+  /**
+   * Event handler for the popupshowing event on the context menu.
+   * This handler is responsible for setting the state on various
+   * menu items in the context menu, and reads values that were sent
+   * up from the frame script and stashed into this.contextMenuData.
+   *
+   * @param event
+   *        The popupshowing event for the context menu.
+   */
+  onContextMenuShowing(event) {
+    let copyLinkMenuItem = document.getElementById("context-copyLink");
+    copyLinkMenuItem.hidden = !this.contextMenuData.isLink;
+
+    let copyEmailMenuItem = document.getElementById("context-copyEmail");
+    copyEmailMenuItem.hidden = !this.contextMenuData.isEmail;
+  },
+
+  /**
+   * Called when the user chooses the "Copy Link" or "Copy Email"
+   * menu items in the context menu. Copies the relevant selection
+   * into the system clipboard.
+   */
+  onContextMenuCopyLinkOrEmail() {
+    // It doesn't make any sense to call this if the context menu
+    // isn't open...
+    if (!this.contextMenuData.isOpen) {
+      return;
+    }
+
+    let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+                      .getService(Ci.nsIClipboardHelper);
+    clipboard.copyString(this.contextMenuData.href, document);
   },
-  onExit: function () {
-    document.getElementById("ppBrowser").collapsed = true;
-    gBrowser.collapsed = false;
-    document.getElementById("viewSource-toolbox").hidden = false;
-  }
-}
+
+  /**
+   * Called when the context menu closes, and invalidates any data
+   * that the frame script might have sent up about what the context
+   * menu was opened on.
+   */
+  onContextMenuHidden(event) {
+    this.contextMenuData = {
+      isOpen: false,
+    };
+  },
+
+  /**
+   * For remote browsers, the contextmenu event is received in the
+   * content process, and a message is sent up from the frame script
+   * to ViewSourceChrome, but then it stops. The event does not bubble
+   * up to the point that the popup is opened in the parent process.
+   * ViewSourceChrome is responsible for opening up the context menu in
+   * that case. This is called when we receive the contextmenu message
+   * from the child, and we know that the browser is currently remote.
+   *
+   * @param screenX
+   *        The screenX coordinate to open the popup at.
+   * @param screenY
+   *        The screenY coordinate to open the popup at.
+   */
+  openContextMenu(screenX, screenY) {
+    gContextMenu.openPopupAtScreen(screenX, screenY, true);
+  },
+
+  /**
+   * Loads the source of a URL. This will probably end up hitting the
+   * network.
+   *
+   * @param URL
+   *        A URL string to be opened in the view source browser.
+   */
+  loadURL(URL) {
+    this.mm.sendAsyncMessage("ViewSource:LoadSource", { URL });
+  },
 
-function getWebNavigation()
-{
-  try {
-    return gBrowser.webNavigation;
-  } catch (e) {
-    return null;
-  }
-}
+  /**
+   * Updates any commands that are dependant on command broadcasters.
+   */
+  updateCommands() {
+    let backBroadcaster = document.getElementById("Browser:Back");
+    let forwardBroadcaster = document.getElementById("Browser:Forward");
+
+    if (this.webNav.canGoBack) {
+      backBroadcaster.removeAttribute("disabled");
+    } else {
+      backBroadcaster.setAttribute("disabled", "true");
+    }
+    if (this.webNav.canGoForward) {
+      forwardBroadcaster.removeAttribute("disabled");
+    } else {
+      forwardBroadcaster.setAttribute("disabled", "true");
+    }
+  },
 
-function ViewSourceGoToLine()
-{
-  var input = {value:gLastLineFound};
-  for (;;) {
-    var ok = Services.prompt.prompt(
+  /**
+   * Updates the status displayed in the status bar of the view source window.
+   *
+   * @param label
+   *        The string to be displayed in the statusbar-lin-col element.
+   */
+  updateStatus(label) {
+    let statusBarField = document.getElementById("statusbar-line-col");
+    if (statusBarField) {
+      statusBarField.label = label;
+    }
+  },
+
+  /**
+   * Opens the "Go to line" prompt for a user to hop to a particular line
+   * of the source code they're viewing. This will keep prompting until the
+   * user either cancels out of the prompt, or enters a valid line number.
+   */
+  promptAndGoToLine() {
+    let input = { value: this.lastLineFound };
+
+    let ok = Services.prompt.prompt(
         window,
         gViewSourceBundle.getString("goToLineTitle"),
         gViewSourceBundle.getString("goToLineText"),
         input,
         null,
         {value:0});
 
     if (!ok)
       return;
 
-    var line = parseInt(input.value, 10);
+    let line = parseInt(input.value, 10);
 
     if (!(line > 0)) {
       Services.prompt.alert(window,
                             gViewSourceBundle.getString("invalidInputTitle"),
                             gViewSourceBundle.getString("invalidInputText"));
+      this.promptAndGoToLine();
+    } else {
+      this.goToLine(line);
+    }
+  },
 
-      continue;
-    }
+  /**
+   * Go to a particular line of the source code. This act is asynchronous.
+   *
+   * @param lineNumber
+   *        The line number to try to go to to.
+   */
+  goToLine(lineNumber) {
+    this.mm.sendAsyncMessage("ViewSource:GoToLine", { lineNumber });
+  },
 
-    var found = goToLine(line);
+  /**
+   * Called when the frame script reports that a line was successfully gotten
+   * to.
+   *
+   * @param lineNumber
+   *        The line number that we successfully got to.
+   */
+  onGoToLineSuccess(lineNumber) {
+    // We'll pre-populate the "Go to line" prompt with this value the next
+    // time it comes up.
+    this.lastLineFound = lineNumber;
+    document.getElementById("statusbar-line-col").label =
+      gViewSourceBundle.getFormattedString("statusBarLineCol", [lineNumber, 1]);
+  },
 
-    if (found)
-      break;
-
+  /**
+   * Called when the frame script reports that we failed to go to a particular
+   * line. This informs the user that their selection was likely out of range,
+   * and then reprompts the user to try again.
+   */
+  onGoToLineFailed() {
     Services.prompt.alert(window,
                           gViewSourceBundle.getString("outOfRangeTitle"),
                           gViewSourceBundle.getString("outOfRangeText"));
+    this.promptAndGoToLine();
+  },
+
+  /**
+   * Reloads the browser, bypassing the network cache.
+   */
+  reload() {
+    gBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
+                             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+  },
+
+  /**
+   * Closes the view source window.
+   */
+  close() {
+    window.close();
+  },
+
+  /**
+   * Called when the user clicks on the "Wrap Long Lines" menu item, and
+   * flips the user preference for wrapping long lines in the view source
+   * browser.
+   */
+  toggleWrapping() {
+    this.shouldWrap = !this.shouldWrap;
+    Services.prefs.setBoolPref("view_source.wrap_long_lines",
+                               this.shouldWrap);
+    this.mm.sendAsyncMessage("ViewSource:ToggleWrapping");
+  },
+
+  /**
+   * Called when the user clicks on the "Syntax Highlighting" menu item, and
+   * flips the user preference for wrapping long lines in the view source
+   * browser.
+   */
+  toggleSyntaxHighlighting() {
+    this.shouldHighlight = !this.shouldHighlight;
+    // We can't flip this value in the child, since prefs are read-only there.
+    // We flip it here, and then cause a reload in the child to make the change
+    // occur.
+    Services.prefs.setBoolPref("view_source.syntax_highlight",
+                               this.shouldHighlight);
+    this.mm.sendAsyncMessage("ViewSource:ToggleSyntaxHighlighting");
+  },
+
+  /**
+   * Updates the "remote" attribute of the view source browser. This
+   * will remove the browser from the DOM, and then re-add it in the
+   * same place it was taken from.
+   *
+   * @param shouldBeRemote
+   *        True if the browser should be made remote. If the browsers
+   *        remoteness already matches this value, this function does
+   *        nothing.
+   */
+  updateBrowserRemoteness(shouldBeRemote) {
+    if (gBrowser.isRemoteBrowser == shouldBeRemote) {
+      return;
+    }
+
+    let parentNode = gBrowser.parentNode;
+    let nextSibling = gBrowser.nextSibling;
+
+    gBrowser.remove();
+    if (shouldBeRemote) {
+      gBrowser.setAttribute("remote", "true");
+    } else {
+      gBrowser.removeAttribute("remote");
+    }
+    // If nextSibling was null, this will put the browser at
+    // the end of the list.
+    parentNode.insertBefore(gBrowser, nextSibling);
+
+    if (shouldBeRemote) {
+      // We're going to send a message down to the remote browser
+      // to load the source content - however, in order for the
+      // contentWindowAsCPOW and contentDocumentAsCPOW values on
+      // the remote browser to be set, we must set up the
+      // RemoteWebProgress, which is lazily loaded. We only need
+      // contentWindowAsCPOW for the printing support, and this
+      // should go away once bug 1146454 is fixed, since we can
+      // then just pass the outerWindowID of the gBrowser to
+      // PrintUtils.
+      gBrowser.webProgress;
+    }
+  },
+};
+
+ViewSourceChrome.init();
+
+/**
+ * PrintUtils uses this to make Print Preview work.
+ */
+let PrintPreviewListener = {
+  getPrintPreviewBrowser() {
+    let browser = document.getElementById("ppBrowser");
+    if (!browser) {
+      browser = document.createElement("browser");
+      browser.setAttribute("id", "ppBrowser");
+      browser.setAttribute("flex", "1");
+      browser.setAttribute("type", "content");
+
+      let findBar = document.getElementById("FindToolbar");
+      document.getElementById("appcontent")
+              .insertBefore(browser, findBar);
+    }
+
+    return browser;
+  },
+
+  getSourceBrowser() {
+    return gBrowser;
+  },
+
+  getNavToolbox() {
+    return document.getElementById("appcontent");
+  },
+
+  onEnter() {
+    let toolbox = document.getElementById("viewSource-toolbox");
+    toolbox.hidden = true;
+    gBrowser.collapsed = true;
+  },
+
+  onExit() {
+    document.getElementById("ppBrowser").collapsed = true;
+    gBrowser.collapsed = false;
+    document.getElementById("viewSource-toolbox").hidden = false;
+  },
+};
+
+// viewZoomOverlay.js uses this
+function getBrowser() {
+  return gBrowser;
+}
+
+this.__defineGetter__("gPageLoader", function () {
+  var webnav = ViewSourceChrome.webNav;
+  if (!webnav)
+    return null;
+  delete this.gPageLoader;
+  this.gPageLoader = (webnav instanceof Ci.nsIWebPageDescriptor) ? webnav
+                                                                 : null;
+  return this.gPageLoader;
+});
+
+// Strips the |view-source:| for internalSave()
+function ViewSourceSavePage()
+{
+  internalSave(gBrowser.currentURI.spec.replace(/^view-source:/i, ""),
+               null, null, null, null, null, "SaveLinkTitle",
+               null, null, gBrowser.contentDocumentAsCPOW, null,
+               gPageLoader);
+}
+
+// Below are old deprecated functions and variables left behind for
+// compatibility reasons. These will be removed soon via bug 1159293.
+
+this.__defineGetter__("gLastLineFound", function () {
+  Deprecated.warning("gLastLineFound is deprecated - please use " +
+                     "ViewSourceChrome.lastLineFound instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  return ViewSourceChrome.lastLineFound;
+});
+
+function onLoadViewSource() {
+  Deprecated.warning("onLoadViewSource() is deprecated - please use " +
+                     "ViewSourceChrome.onXULLoaded() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.onXULLoaded();
+}
+
+function isHistoryEnabled() {
+  Deprecated.warning("isHistoryEnabled() is deprecated - please use " +
+                     "ViewSourceChrome.historyEnabled instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  return ViewSourceChrome.historyEnabled;
+}
+
+function ViewSourceClose() {
+  Deprecated.warning("ViewSourceClose() is deprecated - please use " +
+                     "ViewSourceChrome.close() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.close();
+}
+
+function ViewSourceReload() {
+  Deprecated.warning("ViewSourceReload() is deprecated - please use " +
+                     "ViewSourceChrome.reload() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.reload();
+}
+
+function getWebNavigation()
+{
+  Deprecated.warning("getWebNavigation() is deprecated - please use " +
+                     "ViewSourceChrome.webNav instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  // The original implementation returned null if anything threw during
+  // the getting of the webNavigation.
+  try {
+    return ViewSourceChrome.webNav;
+  } catch (e) {
+    return null;
   }
 }
 
+function viewSource(url) {
+  Deprecated.warning("viewSource() is deprecated - please use " +
+                     "ViewSourceChrome.loadURL() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.loadURL(url);
+}
+
+function ViewSourceGoToLine()
+{
+  Deprecated.warning("ViewSourceGoToLine() is deprecated - please use " +
+                     "ViewSourceChrome.promptAndGoToLine() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.promptAndGoToLine();
+}
+
 function goToLine(line)
 {
-  var viewsource = window.content.document.body;
-
-  // The source document is made up of a number of pre elements with
-  // id attributes in the format <pre id="line123">, meaning that
-  // the first line in the pre element is number 123.
-  // Do binary search to find the pre element containing the line.
-  // However, in the plain text case, we have only one pre without an
-  // attribute, so assume it begins on line 1.
-
-  var pre;
-  for (var lbound = 0, ubound = viewsource.childNodes.length; ; ) {
-    var middle = (lbound + ubound) >> 1;
-    pre = viewsource.childNodes[middle];
-
-    var firstLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
-
-    if (lbound == ubound - 1) {
-      break;
-    }
-
-    if (line >= firstLine) {
-      lbound = middle;
-    } else {
-      ubound = middle;
-    }
-  }
-
-  var result = {};
-  var found = findLocation(pre, line, null, -1, false, result);
-
-  if (!found) {
-    return false;
-  }
-
-  var selection = window.content.getSelection();
-  selection.removeAllRanges();
-
-  // In our case, the range's startOffset is after "\n" on the previous line.
-  // Tune the selection at the beginning of the next line and do some tweaking
-  // to position the focusNode and the caret at the beginning of the line.
-
-  selection.QueryInterface(Ci.nsISelectionPrivate)
-    .interlinePosition = true;
-
-  selection.addRange(result.range);
-
-  if (!selection.isCollapsed) {
-    selection.collapseToEnd();
-
-    var offset = result.range.startOffset;
-    var node = result.range.startContainer;
-    if (offset < node.data.length) {
-      // The same text node spans across the "\n", just focus where we were.
-      selection.extend(node, offset);
-    }
-    else {
-      // There is another tag just after the "\n", hook there. We need
-      // to focus a safe point because there are edgy cases such as
-      // <span>...\n</span><span>...</span> vs.
-      // <span>...\n<span>...</span></span><span>...</span>
-      node = node.nextSibling ? node.nextSibling : node.parentNode.nextSibling;
-      selection.extend(node, 0);
-    }
-  }
-
-  var selCon = getSelectionController();
-  selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
-  selCon.setCaretVisibilityDuringSelection(true);
-
-  // Scroll the beginning of the line into view.
-  selCon.scrollSelectionIntoView(
-    Ci.nsISelectionController.SELECTION_NORMAL,
-    Ci.nsISelectionController.SELECTION_FOCUS_REGION,
-    true);
-
-  gLastLineFound = line;
-
-  document.getElementById("statusbar-line-col").label =
-    gViewSourceBundle.getFormattedString("statusBarLineCol", [line, 1]);
-
-  return true;
-}
-
-function updateStatusBar()
-{
-  // Reset the coalesce flag.
-  gSelectionListener.timeout = 0;
-
-  var statusBarField = document.getElementById("statusbar-line-col");
-
-  var selection = window.content.getSelection();
-  if (!selection.focusNode) {
-    statusBarField.label = '';
-    return;
-  }
-  if (selection.focusNode.nodeType != Node.TEXT_NODE) {
-    return;
-  }
-
-  var selCon = getSelectionController();
-  selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
-  selCon.setCaretVisibilityDuringSelection(true);
-
-  var interlinePosition = selection.QueryInterface(Ci.nsISelectionPrivate)
-                                   .interlinePosition;
-
-  var result = {};
-  findLocation(null, -1, 
-      selection.focusNode, selection.focusOffset, interlinePosition, result);
-
-  statusBarField.label = gViewSourceBundle.getFormattedString(
-                           "statusBarLineCol", [result.line, result.col]);
-}
-
-// Loops through the text lines in the pre element. The arguments are either
-// (pre, line) or (node, offset, interlinePosition). result is an out
-// argument. If (pre, line) are specified (and node == null), result.range is
-// a range spanning the specified line. If the (node, offset,
-// interlinePosition) are specified, result.line and result.col are the line
-// and column number of the specified offset in the specified node relative to
-// the whole file.
-function findLocation(pre, line, node, offset, interlinePosition, result)
-{
-  if (node && !pre) {
-    // Look upwards to find the current pre element.
-    for (pre = node;
-         pre.nodeName != "PRE";
-         pre = pre.parentNode);
-  }
-
-  // The source document is made up of a number of pre elements with
-  // id attributes in the format <pre id="line123">, meaning that
-  // the first line in the pre element is number 123.
-  // However, in the plain text case, there is only one <pre> without an id,
-  // so assume line 1.
-  var curLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
-
-  // Walk through each of the text nodes and count newlines.
-  var treewalker = window.content.document
-      .createTreeWalker(pre, NodeFilter.SHOW_TEXT, null);
-
-  // The column number of the first character in the current text node.
-  var firstCol = 1;
-
-  var found = false;
-  for (var textNode = treewalker.firstChild();
-       textNode && !found;
-       textNode = treewalker.nextNode()) {
-
-    // \r is not a valid character in the DOM, so we only check for \n.
-    var lineArray = textNode.data.split(/\n/);
-    var lastLineInNode = curLine + lineArray.length - 1;
-
-    // Check if we can skip the text node without further inspection.
-    if (node ? (textNode != node) : (lastLineInNode < line)) {
-      if (lineArray.length > 1) {
-        firstCol = 1;
-      }
-      firstCol += lineArray[lineArray.length - 1].length;
-      curLine = lastLineInNode;
-      continue;
-    }
-
-    // curPos is the offset within the current text node of the first
-    // character in the current line.
-    for (var i = 0, curPos = 0;
-         i < lineArray.length;
-         curPos += lineArray[i++].length + 1) {
-
-      if (i > 0) {
-        curLine++;
-      }
-
-      if (node) {
-        if (offset >= curPos && offset <= curPos + lineArray[i].length) {
-          // If we are right after the \n of a line and interlinePosition is
-          // false, the caret looks as if it were at the end of the previous
-          // line, so we display that line and column instead.
-
-          if (i > 0 && offset == curPos && !interlinePosition) {
-            result.line = curLine - 1;
-            var prevPos = curPos - lineArray[i - 1].length;
-            result.col = (i == 1 ? firstCol : 1) + offset - prevPos;
-          } else {
-            result.line = curLine;
-            result.col = (i == 0 ? firstCol : 1) + offset - curPos;
-          }
-          found = true;
-
-          break;
-        }
-
-      } else {
-        if (curLine == line && !("range" in result)) {
-          result.range = document.createRange();
-          result.range.setStart(textNode, curPos);
-
-          // This will always be overridden later, except when we look for
-          // the very last line in the file (this is the only line that does
-          // not end with \n).
-          result.range.setEndAfter(pre.lastChild);
-
-        } else if (curLine == line + 1) {
-          result.range.setEnd(textNode, curPos - 1);
-          found = true;
-          break;
-        }
-      }
-    }
-  }
-
-  return found || ("range" in result);
-}
-
-// Toggle long-line wrapping and sets the view_source.wrap_long_lines
-// pref to persist the last state.
-function wrapLongLines()
-{
-  var myWrap = window.content.document.body;
-  myWrap.classList.toggle("wrap");
-
-  // Since multiple viewsource windows are possible, another window could have
-  // affected the pref, so instead of determining the new pref value via the current
-  // pref value, we use myWrap.classList.
-  Services.prefs.setBoolPref("view_source.wrap_long_lines", myWrap.classList.contains("wrap"));
-}
-
-// Toggles syntax highlighting and sets the view_source.syntax_highlight
-// pref to persist the last state.
-function highlightSyntax()
-{
-  var highlightSyntaxMenu = document.getElementById("menu_highlightSyntax");
-  var highlightSyntax = (highlightSyntaxMenu.getAttribute("checked") == "true");
-  Services.prefs.setBoolPref("view_source.syntax_highlight", highlightSyntax);
-
-  gPageLoader.loadPage(gPageLoader.currentDescriptor, gPageLoader.DISPLAY_NORMAL);
-}
-
-// Reload after change to character encoding or autodetection
-//
-// Fix for bug 136322: this function overrides the function in
-// browser.js to call PageLoader.loadPage() instead of BrowserReloadWithFlags()
-function BrowserCharsetReload()
-{
-  if (isHistoryEnabled()) {
-    gPageLoader.loadPage(gPageLoader.currentDescriptor,
-                         gPageLoader.DISPLAY_NORMAL);
-  } else {
-    gBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
-  }
-}
-
-function BrowserSetCharacterSet(aEvent)
-{
-  if (aEvent.target.hasAttribute("charset"))
-    gBrowser.docShell.charset = aEvent.target.getAttribute("charset");
-  BrowserCharsetReload();
+  Deprecated.warning("goToLine() is deprecated - please use " +
+                     "ViewSourceChrome.goToLine() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.goToLine(line);
 }
 
 function BrowserForward(aEvent) {
-  try {
-    gBrowser.goForward();
-  }
-  catch(ex) {
-  }
+  Deprecated.warning("BrowserForward() is deprecated - please use " +
+                     "ViewSourceChrome.goForward() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.goForward();
 }
 
 function BrowserBack(aEvent) {
-  try {
-    gBrowser.goBack();
-  }
-  catch(ex) {
-  }
+  Deprecated.warning("BrowserBack() is deprecated - please use " +
+                     "ViewSourceChrome.goBack() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.goBack();
 }
 
 function UpdateBackForwardCommands() {
-  var backBroadcaster = document.getElementById("Browser:Back");
-  var forwardBroadcaster = document.getElementById("Browser:Forward");
-
-  if (getWebNavigation().canGoBack)
-    backBroadcaster.removeAttribute("disabled");
-  else
-    backBroadcaster.setAttribute("disabled", "true");
-
-  if (getWebNavigation().canGoForward)
-    forwardBroadcaster.removeAttribute("disabled");
-  else
-    forwardBroadcaster.setAttribute("disabled", "true");
+  Deprecated.warning("UpdateBackForwardCommands() is deprecated - please use " +
+                     "ViewSourceChrome.updateCommands() instead.",
+                     "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/ViewSource");
+  ViewSourceChrome.updateCommands();
 }
-
-function contextMenuShowing() {
-  var isLink = false;
-  var isEmail = false;
-  if (gContextMenu.triggerNode && gContextMenu.triggerNode.localName == 'a') {
-    if (gContextMenu.triggerNode.href.indexOf('view-source:') == 0)
-      isLink = true;
-    if (gContextMenu.triggerNode.href.indexOf('mailto:') == 0)
-      isEmail = true;
-  }
-  document.getElementById('context-copyLink').hidden = !isLink;
-  document.getElementById('context-copyEmail').hidden = !isEmail;
-}
-
-function contextMenuCopyLinkOrEmail() {
-  if (!gContextMenu.triggerNode)
-    return;
-
-  var href = gContextMenu.triggerNode.href;
-  var clipboard = Cc['@mozilla.org/widget/clipboardhelper;1'].
-                  getService(Ci.nsIClipboardHelper);
-  clipboard.copyString(href.substring(href.indexOf(':') + 1), document);
-}
--- a/toolkit/components/viewsource/content/viewSource.xul
+++ b/toolkit/components/viewsource/content/viewSource.xul
@@ -15,17 +15,16 @@
 <!ENTITY % sourceDTD SYSTEM "chrome://global/locale/viewSource.dtd" >
 %sourceDTD;
 <!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetMenu.dtd" >
 %charsetDTD;
 ]>
 
 <window id="viewSource"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        onload="onLoadViewSource();"
         contenttitlesetting="true"
         title="&mainWindow.title;" 
         titlemodifier="&mainWindow.titlemodifier;" 
         titlepreface="&mainWindow.preface;"
         titlemenuseparator ="&mainWindow.titlemodifierseparator;"  
         windowtype="navigator:view-source"
         width="640" height="480"
         screenX="10" screenY="10"
@@ -35,41 +34,41 @@
   <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
   <script type="application/javascript" src="chrome://global/content/viewSource.js"/>
   <script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
   
   <stringbundle id="viewSourceBundle" src="chrome://global/locale/viewSource.properties"/>
 
   <command id="cmd_savePage" oncommand="ViewSourceSavePage();"/>
-  <command id="cmd_print" oncommand="PrintUtils.print();"/>
+  <command id="cmd_print" oncommand="PrintUtils.print(gBrowser.contentWindowAsCPOW, gBrowser);"/>
   <command id="cmd_printpreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/>
   <command id="cmd_pagesetup" oncommand="PrintUtils.showPageSetup();"/>
   <command id="cmd_close" oncommand="window.close();"/>
   <commandset id="editMenuCommands"/>
   <command id="cmd_find"
            oncommand="document.getElementById('FindToolbar').onFindCommand();"/>
   <command id="cmd_findAgain"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(false);"/>
   <command id="cmd_findPrevious"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(true);"/>
 #ifdef XP_MACOSX
   <command id="cmd_findSelection"
            oncommand="document.getElementById('FindToolbar').onFindSelectionCommand();"/>
 #endif
-  <command id="cmd_reload" oncommand="ViewSourceReload();"/>
-  <command id="cmd_goToLine" oncommand="ViewSourceGoToLine();" disabled="true"/>
-  <command id="cmd_highlightSyntax" oncommand="highlightSyntax();"/>
-  <command id="cmd_wrapLongLines" oncommand="wrapLongLines()"/>
+  <command id="cmd_reload" oncommand="ViewSourceChrome.reload();"/>
+  <command id="cmd_goToLine" oncommand="ViewSourceChrome.promptAndGoToLine();" disabled="true"/>
+  <command id="cmd_highlightSyntax" oncommand="ViewSourceChrome.toggleSyntaxHighlighting();"/>
+  <command id="cmd_wrapLongLines" oncommand="ViewSourceChrome.toggleWrapping();"/>
   <command id="cmd_textZoomReduce" oncommand="ZoomManager.reduce();"/>
   <command id="cmd_textZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
   <command id="cmd_textZoomReset" oncommand="ZoomManager.reset();"/>
 
-  <command id="Browser:Back" oncommand="BrowserBack();" observes="viewSourceNavigation"/>
-  <command id="Browser:Forward" oncommand="BrowserForward();" observes="viewSourceNavigation"/>
+  <command id="Browser:Back" oncommand="ViewSourceChrome.goBack()" observes="viewSourceNavigation"/>
+  <command id="Browser:Forward" oncommand="ViewSourceChrome.goForward()" observes="viewSourceNavigation"/>
 
   <broadcaster id="viewSourceNavigation"/>
 
   <keyset id="editMenuKeys"/>
   <keyset id="viewSourceKeys">
     <key id="key_savePage" key="&savePageCmd.commandkey;" modifiers="accel" command="cmd_savePage"/>
     <key id="key_print" key="&printCmd.commandkey;" modifiers="accel" command="cmd_print"/>
     <key id="key_close" key="&closeCmd.commandkey;" modifiers="accel" command="cmd_close"/>
@@ -109,18 +108,17 @@
     <key id="goBackKb2" key="&goBackCmd.commandKey;" command="Browser:Back" modifiers="accel"/>
     <key id="goForwardKb2" key="&goForwardCmd.commandKey;" command="Browser:Forward" modifiers="accel"/>
 #endif
 
   </keyset>
   
   <tooltip id="aHTMLTooltip" page="true"/>
 
-  <menupopup id="viewSourceContextMenu"
-             onpopupshowing="contextMenuShowing();">
+  <menupopup id="viewSourceContextMenu">
     <menuitem id="context-back"
               label="&backCmd.label;"
               accesskey="&backCmd.accesskey;"
               command="Browser:Back"
               observes="viewSourceNavigation"/>
     <menuitem id="context-forward"
               label="&forwardCmd.label;"
               accesskey="&forwardCmd.accesskey;"
@@ -128,21 +126,21 @@
               observes="viewSourceNavigation"/>
     <menuseparator observes="viewSourceNavigation"/>
     <menuitem id="cMenu_findAgain"/>
     <menuseparator/>
     <menuitem id="cMenu_copy"/>
     <menuitem id="context-copyLink"
               label="&copyLinkCmd.label;"
               accesskey="&copyLinkCmd.accesskey;"
-              oncommand="contextMenuCopyLinkOrEmail();"/>
+              oncommand="ViewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
     <menuitem id="context-copyEmail"
               label="&copyEmailCmd.label;"
               accesskey="&copyEmailCmd.accesskey;"
-              oncommand="contextMenuCopyLinkOrEmail();"/>
+              oncommand="ViewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
     <menuseparator/>
     <menuitem id="cMenu_selectAll"/>
   </menupopup>
 
   <!-- Menu --> 
   <toolbox id="viewSource-toolbox">
     <menubar id="viewSource-main-menubar">
 
@@ -203,17 +201,17 @@
                         key="key_textZoomReset"/>
             </menupopup>
           </menu>
 
           <!-- Charset Menu -->
           <menu id="charsetMenu"
                 label="&charsetMenu2.label;"
                 accesskey="&charsetMenu2.accesskey;"
-                oncommand="BrowserSetCharacterSet(event);"
+                oncommand="ViewSourceChrome.onSetCharacterSet(event);"
                 onpopupshowing="CharsetMenu.build(event.target);"
                 onpopupshown="CharsetMenu.update(event.target, content.document.characterSet);">
             <menupopup/>
           </menu>
           <menuseparator/>
           <menuitem id="menu_wrapLongLines" type="checkbox" command="cmd_wrapLongLines"
                     label="&menu_wrapLongLines.title;" accesskey="&menu_wrapLongLines.accesskey;"/>
           <menuitem type="checkbox" id="menu_highlightSyntax" command="cmd_highlightSyntax"
@@ -221,17 +219,17 @@
         </menupopup>
       </menu>
     </menubar>  
   </toolbox>
 
   <vbox id="appcontent" flex="1">
 
     <browser id="content" type="content-primary" name="content" src="about:blank" flex="1"
-             context="viewSourceContextMenu" showcaret="true" tooltip="aHTMLTooltip"/>
+             context="viewSourceContextMenu" showcaret="true" tooltip="aHTMLTooltip" />
     <findbar id="FindToolbar" browserid="content"/>
   </vbox> 
 
   <statusbar id="status-bar" class="chromeclass-status">
     <statusbarpanel id="statusbar-line-col" label="" flex="1"/>
   </statusbar>
 
 </window>
--- a/toolkit/components/viewsource/jar.mn
+++ b/toolkit/components/viewsource/jar.mn
@@ -4,8 +4,9 @@
 
 toolkit.jar:
   content/global/viewSource.css             (content/viewSource.css)
   content/global/viewSource.js              (content/viewSource.js)
 * content/global/viewSource.xul             (content/viewSource.xul)
   content/global/viewPartialSource.js       (content/viewPartialSource.js)
 * content/global/viewPartialSource.xul      (content/viewPartialSource.xul)
   content/global/viewSourceUtils.js         (content/viewSourceUtils.js)
+  content/global/viewSource-content.js      (content/viewSource-content.js)
\ No newline at end of file