summaryrefslogtreecommitdiffstats
path: root/toolkit/components/viewsource/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/viewsource/content')
-rw-r--r--toolkit/components/viewsource/content/viewPartialSource.js22
-rw-r--r--toolkit/components/viewsource/content/viewPartialSource.xul163
-rw-r--r--toolkit/components/viewsource/content/viewSource-content.js978
-rw-r--r--toolkit/components/viewsource/content/viewSource.css11
-rw-r--r--toolkit/components/viewsource/content/viewSource.js884
-rw-r--r--toolkit/components/viewsource/content/viewSource.xul235
-rw-r--r--toolkit/components/viewsource/content/viewSourceUtils.js524
7 files changed, 2817 insertions, 0 deletions
diff --git a/toolkit/components/viewsource/content/viewPartialSource.js b/toolkit/components/viewsource/content/viewPartialSource.js
new file mode 100644
index 000000000..0b069344a
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewPartialSource.js
@@ -0,0 +1,22 @@
+// -*- 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");
+
+function onLoadViewPartialSource() {
+ // check the view_source.wrap_long_lines pref
+ // and set the menuitem's checked attribute accordingly
+ let wrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
+ document.getElementById("menu_wrapLongLines")
+ .setAttribute("checked", wrapLongLines);
+ document.getElementById("menu_highlightSyntax")
+ .setAttribute("checked",
+ Services.prefs.getBoolPref("view_source.syntax_highlight"));
+
+ let args = window.arguments[0];
+ viewSourceChrome.loadViewSourceFromSelection(args.URI, args.drawSelection, args.baseURI);
+ window.content.focus();
+}
diff --git a/toolkit/components/viewsource/content/viewPartialSource.xul b/toolkit/components/viewsource/content/viewPartialSource.xul
new file mode 100644
index 000000000..fdec367b1
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewPartialSource.xul
@@ -0,0 +1,163 @@
+<?xml version="1.0"?>
+# -*- Mode: HTML -*-
+# 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/.
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/content/viewSource.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/viewsource/viewsource.css" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % sourceDTD SYSTEM "chrome://global/locale/viewSource.dtd" >
+%sourceDTD;
+]>
+
+<window id="viewSource"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onLoadViewPartialSource();"
+ contenttitlesetting="true"
+ title="&mainWindow.title;"
+ titlemodifier="&mainWindow.titlemodifier;"
+ titlepreface=""
+ titlemenuseparator ="&mainWindow.titlemodifierseparator;"
+ windowtype="navigator:view-source"
+ width="500" height="300"
+ screenX="10" screenY="10"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <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/viewPartialSource.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.printWindow(gBrowser.outerWindowID, 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);"/>
+ <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"/>
+ <key id="key_close" key="&closeCmd.commandkey;" modifiers="accel" command="cmd_close"/>
+ <key keycode="VK_ESCAPE" command="cmd_close"/>
+
+ <key id="key_textZoomEnlarge" key="&textEnlarge.commandkey;" command="cmd_textZoomEnlarge" modifiers="accel"/>
+ <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">
+ <menuitem id="cMenu_findAgain"/>
+ <menuseparator/>
+ <menuitem id="cMenu_copy"/>
+ <menuitem id="context-copyLink"
+ label="&copyLinkCmd.label;"
+ accesskey="&copyLinkCmd.accesskey;"
+ oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+ <menuitem id="context-copyEmail"
+ label="&copyEmailCmd.label;"
+ accesskey="&copyEmailCmd.accesskey;"
+ oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+
+ <!-- Menu -->
+ <toolbox id="viewSource-toolbox">
+ <menubar id="viewSource-main-menubar">
+
+ <menu id="menu_file" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
+ <menupopup id="menu_FilePopup">
+ <menuitem key="key_savePage" command="cmd_savePage" id="menu_savePage"
+ label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey;"/>
+ <menuitem command="cmd_pagesetup" id="menu_pageSetup"
+ label="&pageSetupCmd.label;" accesskey="&pageSetupCmd.accesskey;"/>
+#ifndef XP_MACOSX
+ <menuitem command="cmd_printpreview" id="menu_printPreview"
+ label="&printPreviewCmd.label;" accesskey="&printPreviewCmd.accesskey;"/>
+#endif
+ <menuitem key="key_print" command="cmd_print" id="menu_print"
+ label="&printCmd.label;" accesskey="&printCmd.accesskey;"/>
+ <menuseparator/>
+ <menuitem key="key_close" command="cmd_close" id="menu_close"
+ label="&closeCmd.label;" accesskey="&closeCmd.accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_edit">
+ <menupopup id="editmenu-popup">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"/>
+ <menuitem id="menu_copy"/>
+ <menuitem id="menu_paste"/>
+ <menuitem id="menu_delete"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"/>
+ <menuseparator/>
+ <menuitem id="menu_find"/>
+ <menuitem id="menu_findAgain"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_view" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
+ <menupopup id="viewmenu-popup">
+ <menu id="viewTextZoomMenu" label="&menu_textSize.label;" accesskey="&menu_textSize.accesskey;">
+ <menupopup>
+ <menuitem id="menu_textEnlarge" command="cmd_textZoomEnlarge"
+ label="&menu_textEnlarge.label;" accesskey="&menu_textEnlarge.accesskey;"
+ key="key_textZoomEnlarge"/>
+ <menuitem id="menu_textReduce" command="cmd_textZoomReduce"
+ label="&menu_textReduce.label;" accesskey="&menu_textReduce.accesskey;"
+ key="key_textZoomReduce"/>
+ <menuseparator/>
+ <menuitem id="menu_textReset" command="cmd_textZoomReset"
+ label="&menu_textReset.label;" accesskey="&menu_textReset.accesskey;"
+ key="key_textZoomReset"/>
+ </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"
+ 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" />
+ <findbar id="FindToolbar" browserid="content"/>
+ </vbox>
+
+</window>
diff --git a/toolkit/components/viewsource/content/viewSource-content.js b/toolkit/components/viewsource/content/viewSource-content.js
new file mode 100644
index 000000000..4efa1e952
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource-content.js
@@ -0,0 +1,978 @@
+/* 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/. */
+
+var { 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 NS_XHTML = "http://www.w3.org/1999/xhtml";
+const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
+
+// These are markers used to delimit the selection during processing. They
+// are removed from the final rendering.
+// We use noncharacter Unicode codepoints to minimize the risk of clashing
+// with anything that might legitimately be present in the document.
+// U+FDD0..FDEF <noncharacters>
+const MARK_SELECTION_START = "\uFDD0";
+const MARK_SELECTION_END = "\uFDEF";
+
+var global = this;
+
+/**
+ * ViewSourceContent should be loaded in the <xul:browser> of the
+ * view source window, and initialized as soon as it has loaded.
+ */
+var 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:LoadSourceWithSelection",
+ "ViewSource:GoToLine",
+ "ViewSource:ToggleWrapping",
+ "ViewSource:ToggleSyntaxHighlighting",
+ "ViewSource:SetCharacterSet",
+ ],
+
+ /**
+ * When showing selection source, chrome will construct a page fragment to
+ * show, and then instruct content to draw a selection after load. This is
+ * set true when there is a pending request to draw selection.
+ */
+ needsDrawSelection: false,
+
+ /**
+ * 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,
+
+ get isViewSource() {
+ let uri = content.document.documentURI;
+ return uri.startsWith("view-source:") ||
+ (uri.startsWith("data:") && uri.includes("MathML"));
+ },
+
+ get isAboutBlank() {
+ let uri = content.document.documentURI;
+ return uri == "about:blank";
+ },
+
+ /**
+ * 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) {
+ if (!this.isViewSource && !this.isAboutBlank) {
+ return;
+ }
+ 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:LoadSourceWithSelection":
+ this.viewSourceWithSelection(data.URL, data.drawSelection, data.baseURI);
+ 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) {
+ if (!this.isViewSource) {
+ return;
+ }
+ 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);
+ },
+
+ /**
+ * A shortcut to the nsIWebBrowserFind for the content.
+ */
+ get webBrowserFind() {
+ return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebBrowserFind);
+ },
+
+ /**
+ * 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;
+ 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;
+
+ if (forcedCharSet) {
+ try {
+ docShell.charset = forcedCharSet;
+ } catch (e) { /* invalid charset */ }
+ }
+
+ if (lineNumber && lineNumber > 0) {
+ let doneLoading = (event) => {
+ // Ignore possible initial load of about:blank
+ if (this.isAboutBlank ||
+ !content.document.body) {
+ return;
+ }
+ 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 for click events from:
+ * * error page content, which can show up if the user attempts to view the
+ * source of an attack page.
+ * * in-page context menu actions
+ */
+ onClick(event) {
+ let target = event.originalTarget;
+ // Check for content menu actions
+ if (target.id) {
+ this.contextMenuItems.forEach(itemSpec => {
+ if (itemSpec.id !== target.id) {
+ return;
+ }
+ itemSpec.handler.call(this, event);
+ event.stopPropagation();
+ });
+ }
+
+ // Don't trust synthetic events
+ if (!event.isTrusted || event.target.localName != "button")
+ return;
+
+ 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) {
+ let selection = content.getSelection();
+ if (selection) {
+ selection.QueryInterface(Ci.nsISelectionPrivate)
+ .addSelectionListener(this);
+ this.selectionListenerAttached = true;
+ }
+ content.focus();
+
+ // If we need to draw the selection, wait until an actual view source page
+ // has loaded, instead of about:blank.
+ if (this.needsDrawSelection &&
+ content.document.documentURI.startsWith("view-source:")) {
+ this.needsDrawSelection = false;
+ this.drawSelection();
+ }
+
+ if (content.document.body) {
+ this.injectContextMenu();
+ }
+
+ 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. Notifies parent to update the pref.
+ */
+ toggleWrapping() {
+ let body = content.document.body;
+ let state = body.classList.toggle("wrap");
+ sendAsyncMessage("ViewSource:StoreWrapping", { state });
+ },
+
+ /**
+ * Toggles the "highlight" class on the document body, which sets whether
+ * or not syntax highlighting is displayed. Notifies parent to update the
+ * pref.
+ */
+ toggleSyntaxHighlighting() {
+ let body = content.document.body;
+ let state = body.classList.toggle("highlight");
+ sendAsyncMessage("ViewSource:StoreSyntaxHighlighting", { state });
+ },
+
+ /**
+ * 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 });
+ },
+
+ /**
+ * Loads a view source selection showing the given view-source url and
+ * highlight the selection.
+ *
+ * @param uri view-source uri to show
+ * @param drawSelection true to highlight the selection
+ * @param baseURI base URI of the original document
+ */
+ viewSourceWithSelection(uri, drawSelection, baseURI)
+ {
+ this.needsDrawSelection = drawSelection;
+
+ // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
+ let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT;
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURIWithOptions(uri, loadFlags,
+ null, referrerPolicy, // referrer
+ null, null, // postData, headers
+ Services.io.newURI(baseURI, null, null));
+ },
+
+ /**
+ * 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();
+ },
+
+ /**
+ * Using special markers left in the serialized source, this helper makes the
+ * underlying markup of the selected fragment to automatically appear as
+ * selected on the inflated view-source DOM.
+ */
+ drawSelection() {
+ content.document.title =
+ this.bundle.GetStringFromName("viewSelectionSourceTitle");
+
+ // find the special selection markers that we added earlier, and
+ // draw the selection between the two...
+ var findService = null;
+ try {
+ // get the find service which stores the global find state
+ findService = Cc["@mozilla.org/find/find_service;1"]
+ .getService(Ci.nsIFindService);
+ } catch (e) { }
+ if (!findService)
+ return;
+
+ // cache the current global find state
+ var matchCase = findService.matchCase;
+ var entireWord = findService.entireWord;
+ var wrapFind = findService.wrapFind;
+ var findBackwards = findService.findBackwards;
+ var searchString = findService.searchString;
+ var replaceString = findService.replaceString;
+
+ // setup our find instance
+ var findInst = this.webBrowserFind;
+ findInst.matchCase = true;
+ findInst.entireWord = false;
+ findInst.wrapFind = true;
+ findInst.findBackwards = false;
+
+ // ...lookup the start mark
+ findInst.searchString = MARK_SELECTION_START;
+ var startLength = MARK_SELECTION_START.length;
+ findInst.findNext();
+
+ var selection = content.getSelection();
+ if (!selection.rangeCount)
+ return;
+
+ var range = selection.getRangeAt(0);
+
+ var startContainer = range.startContainer;
+ var startOffset = range.startOffset;
+
+ // ...lookup the end mark
+ findInst.searchString = MARK_SELECTION_END;
+ var endLength = MARK_SELECTION_END.length;
+ findInst.findNext();
+
+ var endContainer = selection.anchorNode;
+ var endOffset = selection.anchorOffset;
+
+ // reset the selection that find has left
+ selection.removeAllRanges();
+
+ // delete the special markers now...
+ endContainer.deleteData(endOffset, endLength);
+ startContainer.deleteData(startOffset, startLength);
+ if (startContainer == endContainer)
+ endOffset -= startLength; // has shrunk if on same text node...
+ range.setEnd(endContainer, endOffset);
+
+ // show the selection and scroll it into view
+ selection.addRange(range);
+ // the default behavior of the selection is to scroll at the end of
+ // the selection, whereas in this situation, it is more user-friendly
+ // to scroll at the beginning. So we override the default behavior here
+ try {
+ this.selectionController.scrollSelectionIntoView(
+ Ci.nsISelectionController.SELECTION_NORMAL,
+ Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
+ true);
+ }
+ catch (e) { }
+
+ // restore the current find state
+ findService.matchCase = matchCase;
+ findService.entireWord = entireWord;
+ findService.wrapFind = wrapFind;
+ findService.findBackwards = findBackwards;
+ findService.searchString = searchString;
+ findService.replaceString = replaceString;
+
+ findInst.matchCase = matchCase;
+ findInst.entireWord = entireWord;
+ findInst.wrapFind = wrapFind;
+ findInst.findBackwards = findBackwards;
+ findInst.searchString = searchString;
+ },
+
+ /**
+ * In-page context menu items that are injected after page load.
+ */
+ contextMenuItems: [
+ {
+ id: "goToLine",
+ accesskey: true,
+ handler() {
+ sendAsyncMessage("ViewSource:PromptAndGoToLine");
+ }
+ },
+ {
+ id: "wrapLongLines",
+ get checked() {
+ return Services.prefs.getBoolPref("view_source.wrap_long_lines");
+ },
+ handler() {
+ this.toggleWrapping();
+ }
+ },
+ {
+ id: "highlightSyntax",
+ get checked() {
+ return Services.prefs.getBoolPref("view_source.syntax_highlight");
+ },
+ handler() {
+ this.toggleSyntaxHighlighting();
+ }
+ },
+ ],
+
+ /**
+ * Add context menu items for view source specific actions.
+ */
+ injectContextMenu() {
+ let doc = content.document;
+
+ let menu = doc.createElementNS(NS_XHTML, "menu");
+ menu.setAttribute("type", "context");
+ menu.setAttribute("id", "actions");
+ doc.body.appendChild(menu);
+ doc.body.setAttribute("contextmenu", "actions");
+
+ this.contextMenuItems.forEach(itemSpec => {
+ let item = doc.createElementNS(NS_XHTML, "menuitem");
+ item.setAttribute("id", itemSpec.id);
+ let labelName = `context_${itemSpec.id}_label`;
+ let label = this.bundle.GetStringFromName(labelName);
+ item.setAttribute("label", label);
+ if ("checked" in itemSpec) {
+ item.setAttribute("type", "checkbox");
+ }
+ if (itemSpec.accesskey) {
+ let accesskeyName = `context_${itemSpec.id}_accesskey`;
+ item.setAttribute("accesskey",
+ this.bundle.GetStringFromName(accesskeyName))
+ }
+ menu.appendChild(item);
+ });
+
+ this.updateContextMenu();
+ },
+
+ /**
+ * Update state of checkbox-style context menu items.
+ */
+ updateContextMenu() {
+ let doc = content.document;
+ this.contextMenuItems.forEach(itemSpec => {
+ if (!("checked" in itemSpec)) {
+ return;
+ }
+ let item = doc.getElementById(itemSpec.id);
+ if (itemSpec.checked) {
+ item.setAttribute("checked", true);
+ } else {
+ item.removeAttribute("checked");
+ }
+ });
+ },
+};
+ViewSourceContent.init();
diff --git a/toolkit/components/viewsource/content/viewSource.css b/toolkit/components/viewsource/content/viewSource.css
new file mode 100644
index 000000000..d03efcc8c
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource.css
@@ -0,0 +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
diff --git a/toolkit/components/viewsource/content/viewSource.js b/toolkit/components/viewsource/content/viewSource.js
new file mode 100644
index 000000000..873e1bcdb
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource.js
@@ -0,0 +1,884 @@
+// -*- 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/. */
+
+var { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ViewSourceBrowser.jsm");
+
+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;
+ });
+});
+
+/**
+ * ViewSourceChrome is the primary interface for interacting with
+ * the view source browser from a self-contained window. It extends
+ * ViewSourceBrowser with additional things needed inside the special window.
+ *
+ * It initializes itself on script load.
+ */
+function ViewSourceChrome() {
+ ViewSourceBrowser.call(this);
+}
+
+ViewSourceChrome.prototype = {
+ __proto__: ViewSourceBrowser.prototype,
+
+ /**
+ * The <browser> that will be displaying the view source content.
+ */
+ get browser() {
+ return gBrowser;
+ },
+
+ /**
+ * 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: {},
+
+ /**
+ * 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: ViewSourceBrowser.prototype.messages.concat([
+ "ViewSource:SourceLoaded",
+ "ViewSource:SourceUnloaded",
+ "ViewSource:Close",
+ "ViewSource:OpenURL",
+ "ViewSource:UpdateStatus",
+ "ViewSource:ContextMenuOpening",
+ ]),
+
+ /**
+ * This called via ViewSourceBrowser's constructor. 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() {
+ this.mm.loadFrameScript("chrome://global/content/viewSource-content.js", true);
+
+ 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);
+
+ ViewSourceBrowser.prototype.init.call(this);
+ },
+
+ /**
+ * This should be called when the window is closing. This function should
+ * clean up event and message listeners.
+ */
+ uninit() {
+ ViewSourceBrowser.prototype.uninit.call(this);
+
+ // "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);
+ Services.els.removeSystemEventListener(this.browser, "dragover", this,
+ true);
+ Services.els.removeSystemEventListener(this.browser, "drop", this, true);
+ },
+
+ /**
+ * 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;
+
+ switch (message.name) {
+ // Begin messages from super class
+ case "ViewSource:PromptAndGoToLine":
+ this.promptAndGoToLine();
+ break;
+ case "ViewSource:GoToLine:Success":
+ this.onGoToLineSuccess(data.lineNumber);
+ break;
+ case "ViewSource:GoToLine:Failed":
+ this.onGoToLineFailed();
+ break;
+ case "ViewSource:StoreWrapping":
+ this.storeWrapping(data.state);
+ break;
+ case "ViewSource:StoreSyntaxHighlighting":
+ this.storeSyntaxHighlighting(data.state);
+ break;
+ // End messages from super class
+ 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:UpdateStatus":
+ this.updateStatus(data.label);
+ break;
+ case "ViewSource:ContextMenuOpening":
+ this.onContextMenuOpening(data.isLink, data.isEmail, data.href);
+ if (this.browser.isRemoteBrowser) {
+ this.openContextMenu(data.screenX, data.screenY);
+ }
+ 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 "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;
+ case "dragover":
+ this.onDragOver(event);
+ break;
+ case "drop":
+ this.onDrop(event);
+ break;
+ }
+ },
+
+ /**
+ * Getter that returns whether or not the view source browser
+ * has history enabled on it.
+ */
+ get historyEnabled() {
+ return !this.browser.hasAttribute("disablehistory");
+ },
+
+ /**
+ * Getter for the message manager used to communicate with the view source
+ * browser.
+ *
+ * In this window version of view source, we use the window message manager
+ * for loading scripts and listening for messages 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.
+ */
+ get mm() {
+ return window.messageManager;
+ },
+
+ /**
+ * Getter for the nsIWebNavigation of the view source browser.
+ */
+ get webNav() {
+ return this.browser.webNavigation;
+ },
+
+ /**
+ * Send the browser forward in its history.
+ */
+ goForward() {
+ this.browser.goForward();
+ },
+
+ /**
+ * Send the browser backward in its history.
+ */
+ goBack() {
+ this.browser.goBack();
+ },
+
+ /**
+ * 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");
+ }
+
+ let highlightMenuItem = document.getElementById("menu_highlightSyntax");
+ if (this.shouldHighlight) {
+ highlightMenuItem.setAttribute("checked", "true");
+ }
+
+ gContextMenu.addEventListener("popupshowing", this);
+ gContextMenu.addEventListener("popuphidden", this);
+
+ Services.els.addSystemEventListener(this.browser, "dragover", this, true);
+ Services.els.addSystemEventListener(this.browser, "drop", this, true);
+
+ 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");
+ }
+ }
+
+ // We require the first argument to do any loading of source.
+ // otherwise, we're done.
+ if (!window.arguments[0]) {
+ return undefined;
+ }
+
+ if (typeof window.arguments[0] == "string") {
+ // We're using the deprecated API
+ return this._loadViewSourceDeprecated(window.arguments);
+ }
+
+ // 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];
+
+ // viewPartialSource.js will take care of loading the content in partial mode.
+ if (!args.partial) {
+ this.loadViewSource(args);
+ }
+
+ return undefined;
+ },
+
+ /**
+ * This is the deprecated API for viewSource.xul, for old-timer consumers.
+ * This API might eventually go away.
+ */
+ _loadViewSourceDeprecated(aArguments) {
+ 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/View_Source_for_XUL_Applications");
+ // 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 (aArguments[2]) {
+ let pageDescriptor = aArguments[2];
+ if (Cu.isCrossProcessWrapper(pageDescriptor)) {
+ throw new Error("Cannot pass a CPOW as the page descriptor to viewSource.xul.");
+ }
+ }
+
+ if (this.browser.isRemoteBrowser) {
+ throw new Error("Deprecated view source API should not use a remote browser.");
+ }
+
+ let forcedCharSet;
+ if (aArguments[4] && aArguments[1].startsWith("charset=")) {
+ forcedCharSet = aArguments[1].split("=")[1];
+ }
+
+ this.sendAsyncMessage("ViewSource:LoadSourceDeprecated", {
+ URL: aArguments[0],
+ lineNumber: aArguments[3],
+ forcedCharSet,
+ }, {
+ pageDescriptor: aArguments[2],
+ });
+ },
+
+ /**
+ * 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;
+ }
+ },
+
+ /**
+ * 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;
+ }
+ },
+
+ /**
+ * 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");
+
+ if (this.historyEnabled) {
+ this.updateCommands();
+ }
+
+ this.browser.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");
+ },
+
+ /**
+ * 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.sendAsyncMessage("ViewSource:SetCharacterSet", {
+ charset: charset,
+ doPageLoad: this.historyEnabled,
+ });
+
+ if (!this.historyEnabled) {
+ this.browser
+ .reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+ }
+ }
+ },
+
+ /**
+ * 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 };
+ },
+
+ /**
+ * 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);
+ },
+
+ /**
+ * 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,
+ };
+ },
+
+ /**
+ * Called when the user drags something over the content browser.
+ */
+ onDragOver(event) {
+ // For drags that appear to be internal text (for example, tab drags),
+ // set the dropEffect to 'none'. This prevents the drop even if some
+ // other listener cancelled the event.
+ let types = event.dataTransfer.types;
+ if (types.includes("text/x-moz-text-internal") && !types.includes("text/plain")) {
+ event.dataTransfer.dropEffect = "none";
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ let linkHandler = Cc["@mozilla.org/content/dropped-link-handler;1"]
+ .getService(Ci.nsIDroppedLinkHandler);
+
+ if (linkHandler.canDropLink(event, false)) {
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * Called twhen the user drops something onto the content browser.
+ */
+ onDrop(event) {
+ if (event.defaultPrevented)
+ return;
+
+ let name = { };
+ let linkHandler = Cc["@mozilla.org/content/dropped-link-handler;1"]
+ .getService(Ci.nsIDroppedLinkHandler);
+ let uri;
+ try {
+ // Pass true to prevent the dropping of javascript:/data: URIs
+ uri = linkHandler.dropLink(event, name, true);
+ } catch (e) {
+ return;
+ }
+
+ if (uri) {
+ this.loadURL(uri);
+ }
+ },
+
+ /**
+ * 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.sendAsyncMessage("ViewSource:LoadSource", { URL });
+ },
+
+ /**
+ * 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");
+ }
+ },
+
+ /**
+ * 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;
+ }
+ },
+
+ /**
+ * 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) {
+ ViewSourceBrowser.prototype.onGoToLineSuccess.call(this, lineNumber);
+ document.getElementById("statusbar-line-col").label =
+ gViewSourceBundle.getFormattedString("statusBarLineCol", [lineNumber, 1]);
+ },
+
+ /**
+ * Reloads the browser, bypassing the network cache.
+ */
+ reload() {
+ this.browser.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.
+ */
+ toggleWrapping() {
+ this.shouldWrap = !this.shouldWrap;
+ this.sendAsyncMessage("ViewSource:ToggleWrapping");
+ },
+
+ /**
+ * Called when the user clicks on the "Syntax Highlighting" menu item.
+ */
+ toggleSyntaxHighlighting() {
+ this.shouldHighlight = !this.shouldHighlight;
+ this.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 (this.browser.isRemoteBrowser == shouldBeRemote) {
+ return;
+ }
+
+ let parentNode = this.browser.parentNode;
+ let nextSibling = this.browser.nextSibling;
+
+ // XX Removing and re-adding the browser from and to the DOM strips its
+ // XBL properties. Save and restore relatedBrowser. Note that when we
+ // restore relatedBrowser, there won't yet be a binding or setter. This
+ // works in conjunction with the hack in <xul:browser>'s constructor to
+ // re-get the weak reference to it.
+ let relatedBrowser = this.browser.relatedBrowser;
+
+ this.browser.remove();
+ if (shouldBeRemote) {
+ this.browser.setAttribute("remote", "true");
+ } else {
+ this.browser.removeAttribute("remote");
+ }
+
+ this.browser.relatedBrowser = relatedBrowser;
+
+ // If nextSibling was null, this will put the browser at
+ // the end of the list.
+ parentNode.insertBefore(this.browser, 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 this.browser to
+ // PrintUtils.
+ this.browser.webProgress;
+ }
+ },
+};
+
+var viewSourceChrome = new ViewSourceChrome();
+
+/**
+ * PrintUtils uses this to make Print Preview work.
+ */
+var PrintPreviewListener = {
+ _ppBrowser: null,
+
+ getPrintPreviewBrowser() {
+ if (!this._ppBrowser) {
+ this._ppBrowser = document.createElement("browser");
+ this._ppBrowser.setAttribute("flex", "1");
+ this._ppBrowser.setAttribute("type", "content");
+ }
+
+ if (gBrowser.isRemoteBrowser) {
+ this._ppBrowser.setAttribute("remote", "true");
+ } else {
+ this._ppBrowser.removeAttribute("remote");
+ }
+
+ let findBar = document.getElementById("FindToolbar");
+ document.getElementById("appcontent")
+ .insertBefore(this._ppBrowser, findBar);
+
+ return this._ppBrowser;
+ },
+
+ getSourceBrowser() {
+ return gBrowser;
+ },
+
+ getNavToolbox() {
+ return document.getElementById("appcontent");
+ },
+
+ onEnter() {
+ let toolbox = document.getElementById("viewSource-toolbox");
+ toolbox.hidden = true;
+ gBrowser.collapsed = true;
+ },
+
+ onExit() {
+ this._ppBrowser.remove();
+ gBrowser.collapsed = false;
+ document.getElementById("viewSource-toolbox").hidden = false;
+ },
+
+ activateBrowser(browser) {
+ browser.docShellIsActive = true;
+ },
+};
+
+// 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/View_Source_for_XUL_Applications");
+ 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/View_Source_for_XUL_Applications");
+ viewSourceChrome.onXULLoaded();
+}
+
+function isHistoryEnabled() {
+ Deprecated.warning("isHistoryEnabled() is deprecated - please use " +
+ "viewSourceChrome.historyEnabled instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ 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/View_Source_for_XUL_Applications");
+ viewSourceChrome.close();
+}
+
+function ViewSourceReload() {
+ Deprecated.warning("ViewSourceReload() is deprecated - please use " +
+ "viewSourceChrome.reload() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.reload();
+}
+
+function getWebNavigation()
+{
+ Deprecated.warning("getWebNavigation() is deprecated - please use " +
+ "viewSourceChrome.webNav instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ // 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/View_Source_for_XUL_Applications");
+ 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/View_Source_for_XUL_Applications");
+ viewSourceChrome.promptAndGoToLine();
+}
+
+function goToLine(line)
+{
+ Deprecated.warning("goToLine() is deprecated - please use " +
+ "viewSourceChrome.goToLine() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.goToLine(line);
+}
+
+function BrowserForward(aEvent) {
+ Deprecated.warning("BrowserForward() is deprecated - please use " +
+ "viewSourceChrome.goForward() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.goForward();
+}
+
+function BrowserBack(aEvent) {
+ Deprecated.warning("BrowserBack() is deprecated - please use " +
+ "viewSourceChrome.goBack() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.goBack();
+}
+
+function UpdateBackForwardCommands() {
+ Deprecated.warning("UpdateBackForwardCommands() is deprecated - please use " +
+ "viewSourceChrome.updateCommands() instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
+ viewSourceChrome.updateCommands();
+}
diff --git a/toolkit/components/viewsource/content/viewSource.xul b/toolkit/components/viewsource/content/viewSource.xul
new file mode 100644
index 000000000..c6ca58234
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSource.xul
@@ -0,0 +1,235 @@
+<?xml version="1.0"?>
+# -*- Mode: HTML -*-
+# 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/.
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/content/viewSource.css" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/viewsource/viewsource.css" type="text/css"?>
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!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"
+ 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"
+ persist="screenX screenY width height sizemode">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <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.printWindow(gBrowser.outerWindowID, 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="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="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"/>
+ <key id="key_goToLine" key="&goToLineCmd.commandkey;" command="cmd_goToLine" modifiers="accel"/>
+
+ <key id="key_textZoomEnlarge" key="&textEnlarge.commandkey;" command="cmd_textZoomEnlarge" modifiers="accel"/>
+ <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"/>
+
+ <key id="key_reload" key="&reloadCmd.commandkey;" command="cmd_reload" modifiers="accel"/>
+ <key key="&reloadCmd.commandkey;" command="cmd_reload" modifiers="accel,shift"/>
+ <key keycode="VK_F5" command="cmd_reload"/>
+ <key keycode="VK_F5" command="cmd_reload" modifiers="accel"/>
+ <key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/>
+ <key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/>
+ <key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/>
+#ifdef XP_MACOSX
+ <key id="key_findSelection" key="&findSelectionCmd.commandkey;" command="cmd_findSelection" modifiers="accel"/>
+#endif
+ <key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/>
+ <key keycode="&findAgainCmd.commandkey2;" command="cmd_findPrevious" modifiers="shift"/>
+
+ <key keycode="VK_BACK" command="Browser:Back"/>
+ <key keycode="VK_BACK" command="Browser:Forward" modifiers="shift"/>
+#ifndef XP_MACOSX
+ <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="alt"/>
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="alt"/>
+#else
+ <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="accel" />
+ <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="accel" />
+#endif
+#ifdef XP_UNIX
+ <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">
+ <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;"
+ command="Browser:Forward"
+ 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="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+ <menuitem id="context-copyEmail"
+ label="&copyEmailCmd.label;"
+ accesskey="&copyEmailCmd.accesskey;"
+ oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+ <menuseparator/>
+ <menuitem id="cMenu_selectAll"/>
+ </menupopup>
+
+ <!-- Menu -->
+ <toolbox id="viewSource-toolbox">
+ <menubar id="viewSource-main-menubar">
+
+ <menu id="menu_file" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
+ <menupopup id="menu_FilePopup">
+ <menuitem key="key_savePage" command="cmd_savePage" id="menu_savePage"
+ label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey;"/>
+ <menuitem command="cmd_pagesetup" id="menu_pageSetup"
+ label="&pageSetupCmd.label;" accesskey="&pageSetupCmd.accesskey;"/>
+#ifndef XP_MACOSX
+ <menuitem command="cmd_printpreview" id="menu_printPreview"
+ label="&printPreviewCmd.label;" accesskey="&printPreviewCmd.accesskey;"/>
+#endif
+ <menuitem key="key_print" command="cmd_print" id="menu_print"
+ label="&printCmd.label;" accesskey="&printCmd.accesskey;"/>
+ <menuseparator/>
+ <menuitem key="key_close" command="cmd_close" id="menu_close"
+ label="&closeCmd.label;" accesskey="&closeCmd.accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_edit">
+ <menupopup id="editmenu-popup">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"/>
+ <menuitem id="menu_copy"/>
+ <menuitem id="menu_paste"/>
+ <menuitem id="menu_delete"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"/>
+ <menuseparator/>
+ <menuitem id="menu_find"/>
+ <menuitem id="menu_findAgain"/>
+ <menuseparator/>
+ <menuitem id="menu_goToLine" key="key_goToLine" command="cmd_goToLine"
+ label="&goToLineCmd.label;" accesskey="&goToLineCmd.accesskey;"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_view" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
+ <menupopup id="viewmenu-popup">
+ <menuitem id="menu_reload" command="cmd_reload" accesskey="&reloadCmd.accesskey;"
+ label="&reloadCmd.label;" key="key_reload"/>
+ <menuseparator />
+ <menu id="viewTextZoomMenu" label="&menu_textSize.label;" accesskey="&menu_textSize.accesskey;">
+ <menupopup>
+ <menuitem id="menu_textEnlarge" command="cmd_textZoomEnlarge"
+ label="&menu_textEnlarge.label;" accesskey="&menu_textEnlarge.accesskey;"
+ key="key_textZoomEnlarge"/>
+ <menuitem id="menu_textReduce" command="cmd_textZoomReduce"
+ label="&menu_textReduce.label;" accesskey="&menu_textReduce.accesskey;"
+ key="key_textZoomReduce"/>
+ <menuseparator/>
+ <menuitem id="menu_textReset" command="cmd_textZoomReset"
+ label="&menu_textReset.label;" accesskey="&menu_textReset.accesskey;"
+ key="key_textZoomReset"/>
+ </menupopup>
+ </menu>
+
+ <!-- Charset Menu -->
+ <menu id="charsetMenu"
+ label="&charsetMenu2.label;"
+ accesskey="&charsetMenu2.accesskey;"
+ oncommand="viewSourceChrome.onSetCharacterSet(event);"
+ onpopupshowing="CharsetMenu.build(event.target);
+ 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"
+ 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"
+ 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>
diff --git a/toolkit/components/viewsource/content/viewSourceUtils.js b/toolkit/components/viewsource/content/viewSourceUtils.js
new file mode 100644
index 000000000..5752683e9
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSourceUtils.js
@@ -0,0 +1,524 @@
+// -*- 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/. */
+
+/*
+ * To keep the global namespace safe, don't define global variables and
+ * functions in this file.
+ *
+ * This file silently depends on contentAreaUtils.js for
+ * getDefaultFileName, getNormalizedLeafName and getDefaultExtension
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ViewSourceBrowser",
+ "resource://gre/modules/ViewSourceBrowser.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+var gViewSourceUtils = {
+
+ mnsIWebBrowserPersist: Components.interfaces.nsIWebBrowserPersist,
+ mnsIWebProgress: Components.interfaces.nsIWebProgress,
+ mnsIWebPageDescriptor: Components.interfaces.nsIWebPageDescriptor,
+
+ /**
+ * Opens the view source window.
+ *
+ * @param aArgsOrURL (required)
+ * This is either an Object containing parameters, or a string
+ * URL for the page we want to view the source of. In the latter
+ * case we will be paying attention to the other parameters, as
+ * we will be supporting the old API for this method.
+ * If aArgsOrURL is an Object, the other parameters will be ignored.
+ * aArgsOrURL as an Object can include the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ *
+ * @param aPageDescriptor (deprecated, optional)
+ * Accepted for compatibility reasons, but is otherwise ignored.
+ * @param aDocument (deprecated, optional)
+ * The content document we would like to view the source of. This
+ * function will throw if aDocument is a CPOW.
+ * @param aLineNumber (deprecated, optional)
+ * The line number to focus on once the source is loaded.
+ */
+ viewSource: function(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber)
+ {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ if (prefs.getBoolPref("view_source.editor.external")) {
+ this.openInExternalEditor(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber);
+ } else {
+ this._openInInternalViewer(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber);
+ }
+ },
+
+ /**
+ * Displays view source in the provided <browser>. This allows for non-window
+ * display methods, such as a tab from Firefox.
+ *
+ * @param aArgs
+ * An object with the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * viewSourceBrowser (required):
+ * The browser to display the view source in.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ */
+ viewSourceInBrowser: function(aArgs) {
+ Services.telemetry
+ .getHistogramById("VIEW_SOURCE_IN_BROWSER_OPENED_BOOLEAN")
+ .add(true);
+ let viewSourceBrowser = new ViewSourceBrowser(aArgs.viewSourceBrowser);
+ viewSourceBrowser.loadViewSource(aArgs);
+ },
+
+ /**
+ * Displays view source for a selection from some document in the provided
+ * <browser>. This allows for non-window display methods, such as a tab from
+ * Firefox.
+ *
+ * @param aViewSourceInBrowser
+ * The browser containing the page to view the source of.
+ * @param aTarget
+ * Set to the target node for MathML. Null for other types of elements.
+ * @param aGetBrowserFn
+ * If set, a function that will return a browser to open the source in.
+ * If null, or this function returns null, opens the source in a new window.
+ */
+ viewPartialSourceInBrowser: function(aViewSourceInBrowser, aTarget, aGetBrowserFn) {
+ let mm = aViewSourceInBrowser.messageManager;
+ mm.addMessageListener("ViewSource:GetSelectionDone", function gotSelection(message) {
+ mm.removeMessageListener("ViewSource:GetSelectionDone", gotSelection);
+
+ if (!message.data)
+ return;
+
+ let browserToOpenIn = aGetBrowserFn ? aGetBrowserFn() : null;
+ if (browserToOpenIn) {
+ let viewSourceBrowser = new ViewSourceBrowser(browserToOpenIn);
+ viewSourceBrowser.loadViewSourceFromSelection(message.data.uri, message.data.drawSelection,
+ message.data.baseURI);
+ }
+ else {
+ window.openDialog("chrome://global/content/viewPartialSource.xul",
+ "_blank", "all,dialog=no",
+ {
+ URI: message.data.uri,
+ drawSelection: message.data.drawSelection,
+ baseURI: message.data.baseURI,
+ partial: true,
+ });
+ }
+ });
+
+ mm.sendAsyncMessage("ViewSource:GetSelection", { }, { target: aTarget });
+ },
+
+ // Opens the interval view source viewer
+ _openInInternalViewer: function(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber)
+ {
+ // try to open a view-source window while inheriting the charset (if any)
+ var charset = null;
+ var isForcedCharset = false;
+ if (aDocument) {
+ if (Components.utils.isCrossProcessWrapper(aDocument)) {
+ throw new Error("View Source cannot accept a CPOW as a document.");
+ }
+
+ charset = "charset=" + aDocument.characterSet;
+ try {
+ isForcedCharset =
+ aDocument.defaultView
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils)
+ .docCharsetIsForced;
+ } catch (ex) {
+ }
+ }
+ Services.telemetry
+ .getHistogramById("VIEW_SOURCE_IN_WINDOW_OPENED_BOOLEAN")
+ .add(true);
+ openDialog("chrome://global/content/viewSource.xul",
+ "_blank",
+ "all,dialog=no",
+ aArgsOrURL, charset, aPageDescriptor, aLineNumber, isForcedCharset);
+ },
+
+ buildEditorArgs: function(aPath, aLineNumber) {
+ // Determine the command line arguments to pass to the editor.
+ // We currently support a %LINE% placeholder which is set to the passed
+ // line number (or to 0 if there's none)
+ var editorArgs = [];
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var args = prefs.getCharPref("view_source.editor.args");
+ if (args) {
+ args = args.replace("%LINE%", aLineNumber || "0");
+ // add the arguments to the array (keeping quoted strings intact)
+ const argumentRE = /"([^"]+)"|(\S+)/g;
+ while (argumentRE.test(args))
+ editorArgs.push(RegExp.$1 || RegExp.$2);
+ }
+ editorArgs.push(aPath);
+ return editorArgs;
+ },
+
+ /**
+ * Opens an external editor with the view source content.
+ *
+ * @param aArgsOrURL (required)
+ * This is either an Object containing parameters, or a string
+ * URL for the page we want to view the source of. In the latter
+ * case we will be paying attention to the other parameters, as
+ * we will be supporting the old API for this method.
+ * If aArgsOrURL is an Object, the other parameters will be ignored.
+ * aArgsOrURL as an Object can include the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ *
+ * @param aPageDescriptor (deprecated, optional)
+ * Accepted for compatibility reasons, but is otherwise ignored.
+ * @param aDocument (deprecated, optional)
+ * The content document we would like to view the source of. This
+ * function will throw if aDocument is a CPOW.
+ * @param aLineNumber (deprecated, optional)
+ * The line number to focus on once the source is loaded.
+ * @param aCallBack
+ * A function accepting two arguments:
+ * * result (true = success)
+ * * data object
+ * The function defaults to opening an internal viewer if external
+ * viewing fails.
+ */
+ openInExternalEditor: function(aArgsOrURL, aPageDescriptor, aDocument,
+ aLineNumber, aCallBack) {
+ let data;
+ if (typeof aArgsOrURL == "string") {
+ Deprecated.warning("The arguments you're passing to " +
+ "openInExternalEditor are using an out-of-date API.",
+ "https://developer.mozilla.org/en-US/Add-ons/" +
+ "Code_snippets/View_Source_for_XUL_Applications");
+ if (Components.utils.isCrossProcessWrapper(aDocument)) {
+ throw new Error("View Source cannot accept a CPOW as a document.");
+ }
+ data = {
+ url: aArgsOrURL,
+ pageDescriptor: aPageDescriptor,
+ doc: aDocument,
+ lineNumber: aLineNumber,
+ isPrivate: false,
+ };
+ if (aDocument) {
+ data.isPrivate =
+ PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView);
+ }
+ } else {
+ let { URL, browser, lineNumber } = aArgsOrURL;
+ data = {
+ url: URL,
+ lineNumber,
+ isPrivate: false,
+ };
+ if (browser) {
+ data.doc = {
+ characterSet: browser.characterSet,
+ contentType: browser.documentContentType,
+ title: browser.contentTitle,
+ };
+ data.isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
+ }
+ }
+
+ try {
+ var editor = this.getExternalViewSourceEditor();
+ if (!editor) {
+ this.handleCallBack(aCallBack, false, data);
+ return;
+ }
+
+ // make a uri
+ var ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ var charset = data.doc ? data.doc.characterSet : null;
+ var uri = ios.newURI(data.url, charset, null);
+ data.uri = uri;
+
+ var path;
+ var contentType = data.doc ? data.doc.contentType : null;
+ if (uri.scheme == "file") {
+ // it's a local file; we can open it directly
+ path = uri.QueryInterface(Components.interfaces.nsIFileURL).file.path;
+
+ var editorArgs = this.buildEditorArgs(path, data.lineNumber);
+ editor.runw(false, editorArgs, editorArgs.length);
+ this.handleCallBack(aCallBack, true, data);
+ } else {
+ // set up the progress listener with what we know so far
+ this.viewSourceProgressListener.contentLoaded = false;
+ this.viewSourceProgressListener.editor = editor;
+ this.viewSourceProgressListener.callBack = aCallBack;
+ this.viewSourceProgressListener.data = data;
+ if (!data.pageDescriptor) {
+ // without a page descriptor, loadPage has no chance of working. download the file.
+ var file = this.getTemporaryFile(uri, data.doc, contentType);
+ this.viewSourceProgressListener.file = file;
+
+ var webBrowserPersist = Components
+ .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(this.mnsIWebBrowserPersist);
+ // the default setting is to not decode. we need to decode.
+ webBrowserPersist.persistFlags = this.mnsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
+ webBrowserPersist.progressListener = this.viewSourceProgressListener;
+ let referrerPolicy = Components.interfaces.nsIHttpChannel.REFERRER_POLICY_NO_REFERRER;
+ webBrowserPersist.savePrivacyAwareURI(uri, null, null, referrerPolicy, null, null, file, data.isPrivate);
+
+ let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Components.interfaces.nsPIExternalAppLauncher);
+ if (data.isPrivate) {
+ // register the file to be deleted when possible
+ helperService.deleteTemporaryPrivateFileWhenPossible(file);
+ } else {
+ // register the file to be deleted on app exit
+ helperService.deleteTemporaryFileOnExit(file);
+ }
+ } else {
+ // we'll use nsIWebPageDescriptor to get the source because it may
+ // not have to refetch the file from the server
+ // XXXbz this is so broken... This code doesn't set up this docshell
+ // at all correctly; if somehow the view-source stuff managed to
+ // execute script we'd be in big trouble here, I suspect.
+ var webShell = Components.classes["@mozilla.org/docshell;1"].createInstance();
+ webShell.QueryInterface(Components.interfaces.nsIBaseWindow).create();
+ this.viewSourceProgressListener.webShell = webShell;
+ var progress = webShell.QueryInterface(this.mnsIWebProgress);
+ progress.addProgressListener(this.viewSourceProgressListener,
+ this.mnsIWebProgress.NOTIFY_STATE_DOCUMENT);
+ var pageLoader = webShell.QueryInterface(this.mnsIWebPageDescriptor);
+ pageLoader.loadPage(data.pageDescriptor, this.mnsIWebPageDescriptor.DISPLAY_AS_SOURCE);
+ }
+ }
+ } catch (ex) {
+ // we failed loading it with the external editor.
+ Components.utils.reportError(ex);
+ this.handleCallBack(aCallBack, false, data);
+ return;
+ }
+ },
+
+ // Default callback - opens the internal viewer if the external editor failed
+ internalViewerFallback: function(result, data)
+ {
+ if (!result) {
+ this._openInInternalViewer(data.url, data.pageDescriptor, data.doc, data.lineNumber);
+ }
+ },
+
+ // Calls the callback, keeping in mind undefined or null values.
+ handleCallBack: function(aCallBack, result, data)
+ {
+ Services.telemetry
+ .getHistogramById("VIEW_SOURCE_EXTERNAL_RESULT_BOOLEAN")
+ .add(result);
+ // if callback is undefined, default to the internal viewer
+ if (aCallBack === undefined) {
+ this.internalViewerFallback(result, data);
+ } else if (aCallBack) {
+ aCallBack(result, data);
+ }
+ },
+
+ // Returns nsIProcess of the external view source editor or null
+ getExternalViewSourceEditor: function()
+ {
+ try {
+ let viewSourceAppPath =
+ Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch)
+ .getComplexValue("view_source.editor.path",
+ Components.interfaces.nsIFile);
+ let editor = Components.classes['@mozilla.org/process/util;1']
+ .createInstance(Components.interfaces.nsIProcess);
+ editor.init(viewSourceAppPath);
+
+ return editor;
+ }
+ catch (ex) {
+ Components.utils.reportError(ex);
+ }
+
+ return null;
+ },
+
+ viewSourceProgressListener: {
+
+ mnsIWebProgressListener: Components.interfaces.nsIWebProgressListener,
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(this.mnsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+
+ destroy: function() {
+ if (this.webShell) {
+ this.webShell.QueryInterface(Components.interfaces.nsIBaseWindow).destroy();
+ }
+ this.webShell = null;
+ this.editor = null;
+ this.callBack = null;
+ this.data = null;
+ this.file = null;
+ },
+
+ // This listener is used both for tracking the progress of an HTML parse
+ // in one case and for tracking the progress of nsIWebBrowserPersist in
+ // another case.
+ onStateChange: function(aProgress, aRequest, aFlag, aStatus) {
+ // once it's done loading...
+ if ((aFlag & this.mnsIWebProgressListener.STATE_STOP) && aStatus == 0) {
+ if (!this.webShell) {
+ // We aren't waiting for the parser. Instead, we are waiting for
+ // an nsIWebBrowserPersist.
+ this.onContentLoaded();
+ return 0;
+ }
+ var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation);
+ if (webNavigation.document.readyState == "complete") {
+ // This branch is probably never taken. Including it for completeness.
+ this.onContentLoaded();
+ } else {
+ webNavigation.document.addEventListener("DOMContentLoaded",
+ this.onContentLoaded.bind(this));
+ }
+ }
+ return 0;
+ },
+
+ onContentLoaded: function() {
+ // The progress listener may call this multiple times, so be sure we only
+ // run once.
+ if (this.contentLoaded) {
+ return;
+ }
+ try {
+ if (!this.file) {
+ // it's not saved to file yet, it's in the webshell
+
+ // get a temporary filename using the attributes from the data object that
+ // openInExternalEditor gave us
+ this.file = gViewSourceUtils.getTemporaryFile(this.data.uri, this.data.doc,
+ this.data.doc.contentType);
+
+ // we have to convert from the source charset.
+ var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation);
+ var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Components.interfaces.nsIFileOutputStream);
+ foStream.init(this.file, 0x02 | 0x08 | 0x20, -1, 0); // write | create | truncate
+ var coStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterOutputStream);
+ coStream.init(foStream, this.data.doc.characterSet, 0, null);
+
+ // write the source to the file
+ coStream.writeString(webNavigation.document.body.textContent);
+
+ // clean up
+ coStream.close();
+ foStream.close();
+
+ let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Components.interfaces.nsPIExternalAppLauncher);
+ if (this.data.isPrivate) {
+ // register the file to be deleted when possible
+ helperService.deleteTemporaryPrivateFileWhenPossible(this.file);
+ } else {
+ // register the file to be deleted on app exit
+ helperService.deleteTemporaryFileOnExit(this.file);
+ }
+ }
+
+ var editorArgs = gViewSourceUtils.buildEditorArgs(this.file.path,
+ this.data.lineNumber);
+ this.editor.runw(false, editorArgs, editorArgs.length);
+
+ this.contentLoaded = true;
+ gViewSourceUtils.handleCallBack(this.callBack, true, this.data);
+ } catch (ex) {
+ // we failed loading it with the external editor.
+ Components.utils.reportError(ex);
+ gViewSourceUtils.handleCallBack(this.callBack, false, this.data);
+ } finally {
+ this.destroy();
+ }
+ },
+
+ onLocationChange: function() { return 0; },
+ onProgressChange: function() { return 0; },
+ onStatusChange: function() { return 0; },
+ onSecurityChange: function() { return 0; },
+
+ webShell: null,
+ editor: null,
+ callBack: null,
+ data: null,
+ file: null
+ },
+
+ // returns an nsIFile for the passed document in the system temp directory
+ getTemporaryFile: function(aURI, aDocument, aContentType) {
+ // include contentAreaUtils.js in our own context when we first need it
+ if (!this._caUtils) {
+ var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Components.interfaces.mozIJSSubScriptLoader);
+ this._caUtils = {};
+ scriptLoader.loadSubScript("chrome://global/content/contentAreaUtils.js", this._caUtils);
+ }
+
+ var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties);
+ var tempFile = fileLocator.get("TmpD", Components.interfaces.nsIFile);
+ var fileName = this._caUtils.getDefaultFileName(null, aURI, aDocument, aContentType);
+ var extension = this._caUtils.getDefaultExtension(fileName, aURI, aContentType);
+ var leafName = this._caUtils.getNormalizedLeafName(fileName, extension);
+ tempFile.append(leafName);
+ return tempFile;
+ }
+}