summaryrefslogtreecommitdiffstats
path: root/toolkit/components/viewsource
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/viewsource')
-rw-r--r--toolkit/components/viewsource/ViewSourceBrowser.jsm331
-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
-rw-r--r--toolkit/components/viewsource/jar.mn12
-rw-r--r--toolkit/components/viewsource/moz.build17
-rw-r--r--toolkit/components/viewsource/test/.eslintrc.js7
-rw-r--r--toolkit/components/viewsource/test/browser/.eslintrc.js7
-rw-r--r--toolkit/components/viewsource/test/browser/browser.ini12
-rw-r--r--toolkit/components/viewsource/test/browser/browser_bug464222.js12
-rw-r--r--toolkit/components/viewsource/test/browser/browser_bug699356.js19
-rw-r--r--toolkit/components/viewsource/test/browser/browser_bug713810.js23
-rw-r--r--toolkit/components/viewsource/test/browser/browser_contextmenu.js107
-rw-r--r--toolkit/components/viewsource/test/browser/browser_gotoline.js36
-rw-r--r--toolkit/components/viewsource/test/browser/browser_srcdoc.js30
-rw-r--r--toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js136
-rw-r--r--toolkit/components/viewsource/test/browser/file_bug464222.html1
-rw-r--r--toolkit/components/viewsource/test/browser/head.js200
-rw-r--r--toolkit/components/viewsource/test/chrome.ini4
-rw-r--r--toolkit/components/viewsource/test/file_empty.html1
-rw-r--r--toolkit/components/viewsource/test/test_bug428653.html45
25 files changed, 3817 insertions, 0 deletions
diff --git a/toolkit/components/viewsource/ViewSourceBrowser.jsm b/toolkit/components/viewsource/ViewSourceBrowser.jsm
new file mode 100644
index 000000000..0623e244a
--- /dev/null
+++ b/toolkit/components/viewsource/ViewSourceBrowser.jsm
@@ -0,0 +1,331 @@
+// -*- 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/. */
+
+const { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+ "resource://gre/modules/Deprecated.jsm");
+
+const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
+
+const FRAME_SCRIPT = "chrome://global/content/viewSource-content.js";
+
+this.EXPORTED_SYMBOLS = ["ViewSourceBrowser"];
+
+// Keep a set of browsers we've seen before, so we can load our frame script as
+// needed into any new ones.
+var gKnownBrowsers = new WeakSet();
+
+/**
+ * ViewSourceBrowser manages the view source <browser> from the chrome side.
+ * It's companion frame script, viewSource-content.js, needs to be loaded as a
+ * frame script into the browser being managed.
+ *
+ * For a view source window using viewSource.xul, the script viewSource.js in
+ * the window extends an instance of this with more window specific functions.
+ * The page script takes care of loading the companion frame script.
+ *
+ * For a view source tab (or some other non-window case), an instance of this is
+ * created by viewSourceUtils.js to wrap the <browser>. The frame script will
+ * be loaded by this module at construction time.
+ */
+this.ViewSourceBrowser = function ViewSourceBrowser(aBrowser) {
+ this._browser = aBrowser;
+ this.init();
+}
+
+ViewSourceBrowser.prototype = {
+ /**
+ * The <browser> that will be displaying the view source content.
+ */
+ get browser() {
+ return this._browser;
+ },
+
+ /**
+ * Holds the value of the last line found via the "Go to line"
+ * command, to pre-populate the prompt the next time it is
+ * opened.
+ */
+ lastLineFound: null,
+
+ /**
+ * These are the messages that ViewSourceBrowser will listen for
+ * from the frame script it injects. Any message names added here
+ * will automatically have ViewSourceBrowser listen for those messages,
+ * and remove the listeners on teardown.
+ */
+ messages: [
+ "ViewSource:PromptAndGoToLine",
+ "ViewSource:GoToLine:Success",
+ "ViewSource:GoToLine:Failed",
+ "ViewSource:StoreWrapping",
+ "ViewSource:StoreSyntaxHighlighting",
+ ],
+
+ /**
+ * 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.messages.forEach((msgName) => {
+ this.mm.addMessageListener(msgName, this);
+ });
+
+ // If we have a known <browser> already, load the frame script here. This
+ // is not true for the window case, as the element does not exist until the
+ // XUL document loads. For that case, the frame script is loaded by
+ // viewSource.js.
+ if (this._browser) {
+ this.loadFrameScript();
+ }
+ },
+
+ /**
+ * This should be called when the window is closing. This function should
+ * clean up event and message listeners.
+ */
+ uninit() {
+ this.messages.forEach((msgName) => {
+ this.mm.removeMessageListener(msgName, this);
+ });
+ },
+
+ /**
+ * For a new browser we've not seen before, load the frame script.
+ */
+ loadFrameScript() {
+ if (!gKnownBrowsers.has(this.browser)) {
+ gKnownBrowsers.add(this.browser);
+ this.mm.loadFrameScript(FRAME_SCRIPT, false);
+ }
+ },
+
+ /**
+ * 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) {
+ 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;
+ }
+ },
+
+ /**
+ * Getter for the message manager of the view source browser.
+ */
+ get mm() {
+ return this.browser.messageManager;
+ },
+
+ /**
+ * Send a message to the view source browser.
+ */
+ sendAsyncMessage(...args) {
+ this.browser.messageManager.sendAsyncMessage(...args);
+ },
+
+ /**
+ * A getter for the view source string bundle.
+ */
+ get bundle() {
+ if (this._bundle) {
+ return this._bundle;
+ }
+ return this._bundle = Services.strings.createBundle(BUNDLE_URL);
+ },
+
+ /**
+ * Loads the source for a URL while applying some optional features if
+ * enabled.
+ *
+ * For the viewSource.xul window, this is called by onXULLoaded above.
+ * For view source in a specific browser, this is manually called after
+ * this object is constructed.
+ *
+ * This takes a single object argument containing:
+ *
+ * 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.
+ */
+ loadViewSource({ URL, browser, outerWindowID, lineNumber }) {
+ if (!URL) {
+ throw new Error("Must supply a URL when opening view source.");
+ }
+
+ if (browser) {
+ this.browser.relatedBrowser = browser;
+
+ // If we're dealing with a remote browser, then the browser
+ // for view source needs to be remote as well.
+ this.updateBrowserRemoteness(browser.isRemoteBrowser);
+ } else if (outerWindowID) {
+ throw new Error("Must supply the browser if passing the outerWindowID");
+ }
+
+ this.sendAsyncMessage("ViewSource:LoadSource",
+ { URL, outerWindowID, lineNumber });
+ },
+
+ /**
+ * 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
+ */
+ loadViewSourceFromSelection(URL, drawSelection, baseURI) {
+ this.sendAsyncMessage("ViewSource:LoadSourceWithSelection",
+ { URL, drawSelection, baseURI });
+ },
+
+ /**
+ * 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) {
+ // In this base case, where we are handed a <browser> someone else is
+ // managing, we don't know for sure that it's safe to toggle remoteness.
+ // For view source in a window, this is overridden to actually do the
+ // flip if needed.
+ throw new Error("View source browser's remoteness mismatch");
+ }
+ },
+
+ /**
+ * Opens the "Go to line" prompt for a user to hop to a particular line
+ * of the source code they're viewing. This will keep prompting until the
+ * user either cancels out of the prompt, or enters a valid line number.
+ */
+ promptAndGoToLine() {
+ let input = { value: this.lastLineFound };
+ let window = Services.wm.getMostRecentWindow(null);
+
+ let ok = Services.prompt.prompt(
+ window,
+ this.bundle.GetStringFromName("goToLineTitle"),
+ this.bundle.GetStringFromName("goToLineText"),
+ input,
+ null,
+ {value:0});
+
+ if (!ok)
+ return;
+
+ let line = parseInt(input.value, 10);
+
+ if (!(line > 0)) {
+ Services.prompt.alert(window,
+ this.bundle.GetStringFromName("invalidInputTitle"),
+ this.bundle.GetStringFromName("invalidInputText"));
+ this.promptAndGoToLine();
+ } else {
+ this.goToLine(line);
+ }
+ },
+
+ /**
+ * Go to a particular line of the source code. This act is asynchronous.
+ *
+ * @param lineNumber
+ * The line number to try to go to to.
+ */
+ goToLine(lineNumber) {
+ this.sendAsyncMessage("ViewSource:GoToLine", { lineNumber });
+ },
+
+ /**
+ * Called when the frame script reports that a line was successfully gotten
+ * to.
+ *
+ * @param lineNumber
+ * The line number that we successfully got to.
+ */
+ onGoToLineSuccess(lineNumber) {
+ // We'll pre-populate the "Go to line" prompt with this value the next
+ // time it comes up.
+ this.lastLineFound = lineNumber;
+ },
+
+ /**
+ * Called when the frame script reports that we failed to go to a particular
+ * line. This informs the user that their selection was likely out of range,
+ * and then reprompts the user to try again.
+ */
+ onGoToLineFailed() {
+ let window = Services.wm.getMostRecentWindow(null);
+ Services.prompt.alert(window,
+ this.bundle.GetStringFromName("outOfRangeTitle"),
+ this.bundle.GetStringFromName("outOfRangeText"));
+ this.promptAndGoToLine();
+ },
+
+ /**
+ * Update the wrapping pref based on the child's current state.
+ * @param state
+ * Whether wrapping is currently enabled in the child.
+ */
+ storeWrapping(state) {
+ Services.prefs.setBoolPref("view_source.wrap_long_lines", state);
+ },
+
+ /**
+ * Update the syntax highlighting pref based on the child's current state.
+ * @param state
+ * Whether syntax highlighting is currently enabled in the child.
+ */
+ storeSyntaxHighlighting(state) {
+ Services.prefs.setBoolPref("view_source.syntax_highlight", state);
+ },
+
+};
+
+/**
+ * Helper to decide if a URI maps to view source content.
+ * @param uri
+ * String containing the URI
+ */
+ViewSourceBrowser.isViewSource = function(uri) {
+ return uri.startsWith("view-source:") ||
+ (uri.startsWith("data:") && uri.includes("MathML"));
+};
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;
+ }
+}
diff --git a/toolkit/components/viewsource/jar.mn b/toolkit/components/viewsource/jar.mn
new file mode 100644
index 000000000..00a1f19a4
--- /dev/null
+++ b/toolkit/components/viewsource/jar.mn
@@ -0,0 +1,12 @@
+# 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/.
+
+toolkit.jar:
+ content/global/viewSource.css (content/viewSource.css)
+ content/global/viewSource.js (content/viewSource.js)
+* content/global/viewSource.xul (content/viewSource.xul)
+ content/global/viewPartialSource.js (content/viewPartialSource.js)
+* content/global/viewPartialSource.xul (content/viewPartialSource.xul)
+ content/global/viewSourceUtils.js (content/viewSourceUtils.js)
+ content/global/viewSource-content.js (content/viewSource-content.js)
diff --git a/toolkit/components/viewsource/moz.build b/toolkit/components/viewsource/moz.build
new file mode 100644
index 000000000..aecd25682
--- /dev/null
+++ b/toolkit/components/viewsource/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+ 'ViewSourceBrowser.jsm',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'View Source')
diff --git a/toolkit/components/viewsource/test/.eslintrc.js b/toolkit/components/viewsource/test/.eslintrc.js
new file mode 100644
index 000000000..2c669d844
--- /dev/null
+++ b/toolkit/components/viewsource/test/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/viewsource/test/browser/.eslintrc.js b/toolkit/components/viewsource/test/browser/.eslintrc.js
new file mode 100644
index 000000000..7c8021192
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/viewsource/test/browser/browser.ini b/toolkit/components/viewsource/test/browser/browser.ini
new file mode 100644
index 000000000..d9ebbd25f
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+support-files = head.js
+ file_bug464222.html
+
+[browser_bug464222.js]
+[browser_bug699356.js]
+[browser_bug713810.js]
+[browser_contextmenu.js]
+subsuite = clipboard
+[browser_gotoline.js]
+[browser_srcdoc.js]
+[browser_viewsourceprefs.js]
diff --git a/toolkit/components/viewsource/test/browser/browser_bug464222.js b/toolkit/components/viewsource/test/browser/browser_bug464222.js
new file mode 100644
index 000000000..30c4fb67a
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_bug464222.js
@@ -0,0 +1,12 @@
+const source = "http://example.com/browser/toolkit/components/viewsource/test/browser/file_bug464222.html";
+
+add_task(function *() {
+ let viewSourceTab = yield* openDocumentSelect(source, "a");
+
+ let href = yield ContentTask.spawn(viewSourceTab.linkedBrowser, { }, function* () {
+ return content.document.querySelectorAll("a[href]")[0].href;
+ });
+
+ is(href, "view-source:" + source, "Relative links broken?");
+ gBrowser.removeTab(viewSourceTab);
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_bug699356.js b/toolkit/components/viewsource/test/browser/browser_bug699356.js
new file mode 100644
index 000000000..e55c4cf20
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_bug699356.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test() {
+ let source = "about:blank";
+
+ waitForExplicitFinish();
+ openViewSourceWindow(source, function(aWindow) {
+ let gBrowser = aWindow.gBrowser;
+ let docEl = aWindow.document.documentElement;
+
+ is(gBrowser.contentDocument.title, source, "Correct document title");
+ is(docEl.getAttribute("title"),
+ "Source of: " + source + ("nsILocalFileMac" in Components.interfaces ? "" : " - " + docEl.getAttribute("titlemodifier")),
+ "Correct window title");
+ closeViewSourceWindow(aWindow, finish);
+ });
+}
diff --git a/toolkit/components/viewsource/test/browser/browser_bug713810.js b/toolkit/components/viewsource/test/browser/browser_bug713810.js
new file mode 100644
index 000000000..d5b2f3424
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_bug713810.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const source = '<html xmlns="http://www.w3.org/1999/xhtml"><body><p>This is a paragraph.</p></body></html>';
+
+add_task(function *() {
+ let viewSourceTab = yield* openDocumentSelect("data:text/html," + source, "p");
+ yield ContentTask.spawn(viewSourceTab.linkedBrowser, null, function* () {
+ Assert.equal(content.document.body.textContent, "<p>This is a paragraph.</p>",
+ "Correct source for text/html");
+ });
+ gBrowser.removeTab(viewSourceTab);
+
+ viewSourceTab = yield* openDocumentSelect("data:application/xhtml+xml," + source, "p");
+ yield ContentTask.spawn(viewSourceTab.linkedBrowser, null, function* () {
+ Assert.equal(content.document.body.textContent,
+ '<p xmlns="http://www.w3.org/1999/xhtml">This is a paragraph.</p>',
+ "Correct source for application/xhtml+xml");
+ });
+ gBrowser.removeTab(viewSourceTab);
+});
+
diff --git a/toolkit/components/viewsource/test/browser/browser_contextmenu.js b/toolkit/components/viewsource/test/browser/browser_contextmenu.js
new file mode 100644
index 000000000..72b8a40be
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_contextmenu.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var source = "data:text/html,text<link%20href='http://example.com/'%20/>more%20text<a%20href='mailto:abc@def.ghi'>email</a>";
+var gViewSourceWindow, gContextMenu, gCopyLinkMenuItem, gCopyEmailMenuItem;
+
+var expectedData = [];
+
+add_task(function *() {
+ // Full source in view source window
+ let newWindow = yield loadViewSourceWindow(source);
+ yield SimpleTest.promiseFocus(newWindow);
+
+ yield* onViewSourceWindowOpen(newWindow, false);
+
+ let contextMenu = gViewSourceWindow.document.getElementById("viewSourceContextMenu");
+
+ for (let test of expectedData) {
+ yield* checkMenuItems(contextMenu, false, test[0], test[1], test[2], test[3]);
+ }
+
+ yield new Promise(resolve => {
+ closeViewSourceWindow(newWindow, resolve);
+ });
+
+ // Selection source in view source tab
+ expectedData = [];
+ let newTab = yield openDocumentSelect(source, "body");
+ yield* onViewSourceWindowOpen(window, true);
+
+ contextMenu = document.getElementById("contentAreaContextMenu");
+
+ for (let test of expectedData) {
+ yield* checkMenuItems(contextMenu, true, test[0], test[1], test[2], test[3]);
+ }
+
+ gBrowser.removeTab(newTab);
+
+ // Selection source in view source window
+ yield pushPrefs(["view_source.tab", false]);
+
+ expectedData = [];
+ newWindow = yield openDocumentSelect(source, "body");
+ yield SimpleTest.promiseFocus(newWindow);
+
+ yield* onViewSourceWindowOpen(newWindow, false);
+
+ contextMenu = newWindow.document.getElementById("viewSourceContextMenu");
+
+ for (let test of expectedData) {
+ yield* checkMenuItems(contextMenu, false, test[0], test[1], test[2], test[3]);
+ }
+
+ yield new Promise(resolve => {
+ closeViewSourceWindow(newWindow, resolve);
+ });
+});
+
+function* onViewSourceWindowOpen(aWindow, aIsTab) {
+ gViewSourceWindow = aWindow;
+
+ gCopyLinkMenuItem = aWindow.document.getElementById(aIsTab ? "context-copylink" : "context-copyLink");
+ gCopyEmailMenuItem = aWindow.document.getElementById(aIsTab ? "context-copyemail" : "context-copyEmail");
+
+ let browser = aIsTab ? gBrowser.selectedBrowser : gViewSourceWindow.gBrowser;
+ yield ContentTask.spawn(browser, null, function* (arg) {
+ let tags = content.document.querySelectorAll("a[href]");
+ Assert.equal(tags[0].href, "view-source:http://example.com/", "Link has correct href");
+ Assert.equal(tags[1].href, "mailto:abc@def.ghi", "Link has correct href");
+ });
+
+ expectedData.push(["a[href]", true, false, "http://example.com/"]);
+ expectedData.push(["a[href^=mailto]", false, true, "abc@def.ghi"]);
+ expectedData.push(["span", false, false, null]);
+}
+
+function* checkMenuItems(contextMenu, isTab, selector, copyLinkExpected, copyEmailExpected, expectedClipboardContent) {
+
+ let browser = isTab ? gBrowser.selectedBrowser : gViewSourceWindow.gBrowser;
+ yield ContentTask.spawn(browser, { selector: selector }, function* (arg) {
+ content.document.querySelector(arg.selector).scrollIntoView();
+ });
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter(selector,
+ { type: "contextmenu", button: 2}, browser);
+ yield popupShownPromise;
+
+ is(gCopyLinkMenuItem.hidden, !copyLinkExpected, "Copy link menuitem is " + (copyLinkExpected ? "not hidden" : "hidden"));
+ is(gCopyEmailMenuItem.hidden, !copyEmailExpected, "Copy email menuitem is " + (copyEmailExpected ? "not hidden" : "hidden"));
+
+ if (copyLinkExpected || copyEmailExpected) {
+ yield new Promise((resolve, reject) => {
+ waitForClipboard(expectedClipboardContent, function() {
+ if (copyLinkExpected)
+ gCopyLinkMenuItem.click();
+ else
+ gCopyEmailMenuItem.click();
+ }, resolve, reject);
+ });
+ }
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.hidePopup();
+ yield popupHiddenPromise;
+}
diff --git a/toolkit/components/viewsource/test/browser/browser_gotoline.js b/toolkit/components/viewsource/test/browser/browser_gotoline.js
new file mode 100644
index 000000000..5bb45f9ca
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_gotoline.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
+
+var content = "line 1\nline 2\nline 3";
+
+add_task(function*() {
+ // First test with text with the text/html mimetype.
+ let win = yield loadViewSourceWindow("data:text/html," + encodeURIComponent(content));
+ yield checkViewSource(win);
+ yield BrowserTestUtils.closeWindow(win);
+
+ win = yield loadViewSourceWindow("data:text/plain," + encodeURIComponent(content));
+ yield checkViewSource(win);
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+var checkViewSource = Task.async(function* (aWindow) {
+ is(aWindow.gBrowser.contentDocument.body.textContent, content, "Correct content loaded");
+ let statusPanel = aWindow.document.getElementById("statusbar-line-col");
+ is(statusPanel.getAttribute("label"), "", "Correct status bar text");
+
+ for (let i = 1; i <= 3; i++) {
+ aWindow.viewSourceChrome.goToLine(i);
+ yield ContentTask.spawn(aWindow.gBrowser, i, function*(i) {
+ let selection = content.getSelection();
+ Assert.equal(selection.toString(), "line " + i, "Correct text selected");
+ });
+
+ yield ContentTaskUtils.waitForCondition(() => {
+ return (statusPanel.getAttribute("label") == "Line " + i + ", Col 1");
+ }, "Correct status bar text");
+ }
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_srcdoc.js b/toolkit/components/viewsource/test/browser/browser_srcdoc.js
new file mode 100644
index 000000000..542741ffc
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_srcdoc.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const frameSource = `<a href="about:mozilla">good</a>`;
+const source = `<html><iframe srcdoc='${frameSource}' id="f"></iframe></html>`;
+
+add_task(function*() {
+ let url = `data:text/html,${source}`;
+ yield BrowserTestUtils.withNewTab({ gBrowser, url }, checkFrameSource);
+});
+
+function* checkFrameSource() {
+ let sourceTab = yield openViewFrameSourceTab("#f");
+ registerCleanupFunction(function() {
+ gBrowser.removeTab(sourceTab);
+ });
+
+ yield waitForSourceLoaded(sourceTab);
+
+ let browser = gBrowser.selectedBrowser;
+ let textContent = yield ContentTask.spawn(browser, {}, function*() {
+ return content.document.body.textContent;
+ });
+ is(textContent, frameSource, "Correct content loaded");
+ let id = yield ContentTask.spawn(browser, {}, function*() {
+ return content.document.body.id;
+ });
+ is(id, "viewsource", "View source mode enabled")
+}
diff --git a/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js b/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
new file mode 100644
index 000000000..7361a70a5
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var plaintextURL = "data:text/plain,hello+world";
+var htmlURL = "about:mozilla";
+
+add_task(function* setup() {
+ registerCleanupFunction(function() {
+ SpecialPowers.clearUserPref("view_source.tab_size");
+ SpecialPowers.clearUserPref("view_source.wrap_long_lines");
+ SpecialPowers.clearUserPref("view_source.syntax_highlight");
+ });
+});
+
+add_task(function*() {
+ yield exercisePrefs(plaintextURL, false);
+ yield exercisePrefs(htmlURL, true);
+});
+
+var exercisePrefs = Task.async(function* (source, highlightable) {
+ let win = yield loadViewSourceWindow(source);
+ let wrapMenuItem = win.document.getElementById("menu_wrapLongLines");
+ let syntaxMenuItem = win.document.getElementById("menu_highlightSyntax");
+
+ // Strip checked="false" attributes, since we're not interested in them.
+ if (wrapMenuItem.getAttribute("checked") == "false") {
+ wrapMenuItem.removeAttribute("checked");
+ }
+ if (syntaxMenuItem.getAttribute("checked") == "false") {
+ syntaxMenuItem.removeAttribute("checked");
+ }
+
+ // Test the default states of these menu items.
+ is(wrapMenuItem.hasAttribute("checked"), false,
+ "Wrap menu item not checked by default");
+ is(syntaxMenuItem.hasAttribute("checked"), true,
+ "Syntax menu item checked by default");
+
+ yield checkStyle(win, "-moz-tab-size", 4);
+ yield checkStyle(win, "white-space", "pre");
+
+ // Next, test that the Wrap Long Lines menu item works.
+ let prefReady = waitForPrefChange("view_source.wrap_long_lines");
+ simulateClick(wrapMenuItem);
+ is(wrapMenuItem.hasAttribute("checked"), true, "Wrap menu item checked");
+ yield prefReady;
+ is(SpecialPowers.getBoolPref("view_source.wrap_long_lines"), true, "Wrap pref set");
+
+ yield checkStyle(win, "white-space", "pre-wrap");
+
+ prefReady = waitForPrefChange("view_source.wrap_long_lines");
+ simulateClick(wrapMenuItem);
+ is(wrapMenuItem.hasAttribute("checked"), false, "Wrap menu item unchecked");
+ yield prefReady;
+ is(SpecialPowers.getBoolPref("view_source.wrap_long_lines"), false, "Wrap pref set");
+ yield checkStyle(win, "white-space", "pre");
+
+ // Check that the Syntax Highlighting menu item works.
+ prefReady = waitForPrefChange("view_source.syntax_highlight");
+ simulateClick(syntaxMenuItem);
+ is(syntaxMenuItem.hasAttribute("checked"), false, "Syntax menu item unchecked");
+ yield prefReady;
+ is(SpecialPowers.getBoolPref("view_source.syntax_highlight"), false, "Syntax highlighting pref set");
+ yield checkHighlight(win, false);
+
+ prefReady = waitForPrefChange("view_source.syntax_highlight");
+ simulateClick(syntaxMenuItem);
+ is(syntaxMenuItem.hasAttribute("checked"), true, "Syntax menu item checked");
+ yield prefReady;
+ is(SpecialPowers.getBoolPref("view_source.syntax_highlight"), true, "Syntax highlighting pref set");
+ yield checkHighlight(win, highlightable);
+ yield BrowserTestUtils.closeWindow(win);
+
+ // Open a new view-source window to check that the prefs are obeyed.
+ SpecialPowers.setIntPref("view_source.tab_size", 2);
+ SpecialPowers.setBoolPref("view_source.wrap_long_lines", true);
+ SpecialPowers.setBoolPref("view_source.syntax_highlight", false);
+
+ win = yield loadViewSourceWindow(source);
+ wrapMenuItem = win.document.getElementById("menu_wrapLongLines");
+ syntaxMenuItem = win.document.getElementById("menu_highlightSyntax");
+
+ // Strip checked="false" attributes, since we're not interested in them.
+ if (wrapMenuItem.getAttribute("checked") == "false") {
+ wrapMenuItem.removeAttribute("checked");
+ }
+ if (syntaxMenuItem.getAttribute("checked") == "false") {
+ syntaxMenuItem.removeAttribute("checked");
+ }
+
+ is(wrapMenuItem.hasAttribute("checked"), true, "Wrap menu item checked");
+ is(syntaxMenuItem.hasAttribute("checked"), false, "Syntax menu item unchecked");
+ yield checkStyle(win, "-moz-tab-size", 2);
+ yield checkStyle(win, "white-space", "pre-wrap");
+ yield checkHighlight(win, false);
+
+ SpecialPowers.clearUserPref("view_source.tab_size");
+ SpecialPowers.clearUserPref("view_source.wrap_long_lines");
+ SpecialPowers.clearUserPref("view_source.syntax_highlight");
+
+ yield BrowserTestUtils.closeWindow(win);
+});
+
+// Simulate a menu item click, including toggling the checked state.
+// This saves us from opening the menu and trying to click on the item,
+// which doesn't work on Mac OS X.
+function simulateClick(aMenuItem) {
+ if (aMenuItem.hasAttribute("checked"))
+ aMenuItem.removeAttribute("checked");
+ else
+ aMenuItem.setAttribute("checked", "true");
+
+ aMenuItem.click();
+}
+
+var checkStyle = Task.async(function* (win, styleProperty, expected) {
+ let browser = win.gBrowser;
+ let value = yield ContentTask.spawn(browser, styleProperty, function* (styleProperty) {
+ let style = content.getComputedStyle(content.document.body, null);
+ return style.getPropertyValue(styleProperty);
+ });
+ is(value, expected, "Correct value of " + styleProperty);
+});
+
+var checkHighlight = Task.async(function* (win, expected) {
+ let browser = win.gBrowser;
+ let highlighted = yield ContentTask.spawn(browser, {}, function* () {
+ let spans = content.document.getElementsByTagName("span");
+ return Array.some(spans, (span) => {
+ let style = content.getComputedStyle(span, null);
+ return style.getPropertyValue("color") !== "rgb(0, 0, 0)";
+ });
+ });
+ is(highlighted, expected, "Syntax highlighting " + (expected ? "on" : "off"));
+});
diff --git a/toolkit/components/viewsource/test/browser/file_bug464222.html b/toolkit/components/viewsource/test/browser/file_bug464222.html
new file mode 100644
index 000000000..4f2a43f0d
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/file_bug464222.html
@@ -0,0 +1 @@
+<a href="file_bug464222.html">I'm a link</a> \ No newline at end of file
diff --git a/toolkit/components/viewsource/test/browser/head.js b/toolkit/components/viewsource/test/browser/head.js
new file mode 100644
index 000000000..bb46369b0
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/head.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+const WINDOW_TYPE = "navigator:view-source";
+
+function openViewSourceWindow(aURI, aCallback) {
+ let viewSourceWindow = openDialog("chrome://global/content/viewSource.xul", null, null, aURI);
+ viewSourceWindow.addEventListener("pageshow", function pageShowHandler(event) {
+ // Wait for the inner window to load, not viewSourceWindow.
+ if (event.target.location == "view-source:" + aURI) {
+ info("View source window opened: " + event.target.location);
+ viewSourceWindow.removeEventListener("pageshow", pageShowHandler, false);
+ aCallback(viewSourceWindow);
+ }
+ }, false);
+}
+
+function loadViewSourceWindow(URL) {
+ return new Promise((resolve) => {
+ openViewSourceWindow(URL, resolve);
+ })
+}
+
+function closeViewSourceWindow(aWindow, aCallback) {
+ Services.wm.addListener({
+ onCloseWindow: function() {
+ Services.wm.removeListener(this);
+ executeSoon(aCallback);
+ }
+ });
+ aWindow.close();
+}
+
+function testViewSourceWindow(aURI, aTestCallback, aCloseCallback) {
+ openViewSourceWindow(aURI, function(aWindow) {
+ aTestCallback(aWindow);
+ closeViewSourceWindow(aWindow, aCloseCallback);
+ });
+}
+
+function waitForViewSourceWindow() {
+ return new Promise(resolve => {
+ let windowListener = {
+ onOpenWindow(xulWindow) {
+ let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener, false);
+ if (win.document.documentElement.getAttribute("windowtype") !=
+ WINDOW_TYPE) {
+ return;
+ }
+ // Found the window
+ resolve(win);
+ Services.wm.removeListener(windowListener);
+ }, false);
+ },
+ onCloseWindow() {},
+ onWindowTitleChange() {}
+ };
+ Services.wm.addListener(windowListener);
+ });
+}
+
+/**
+ * Opens a view source tab / window for a selection (View Selection Source)
+ * within the currently selected browser in gBrowser.
+ *
+ * @param aCSSSelector - used to specify a node within the selection to
+ * view the source of. It is expected that this node is
+ * within an existing selection.
+ * @returns the new tab / window which shows the source.
+ */
+function* openViewPartialSource(aCSSSelector) {
+ let contentAreaContextMenuPopup =
+ document.getElementById("contentAreaContextMenu");
+ let popupShownPromise =
+ BrowserTestUtils.waitForEvent(contentAreaContextMenuPopup, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter(aCSSSelector,
+ { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ let openPromise;
+ if (Services.prefs.getBoolPref("view_source.tab")) {
+ openPromise = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ } else {
+ openPromise = waitForViewSourceWindow();
+ }
+
+ let popupHiddenPromise =
+ BrowserTestUtils.waitForEvent(contentAreaContextMenuPopup, "popuphidden");
+ let item = document.getElementById("context-viewpartialsource-selection");
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ yield popupHiddenPromise;
+
+ return (yield openPromise);
+}
+
+/**
+ * Opens a view source tab for a frame (View Frame Source) within the
+ * currently selected browser in gBrowser.
+ *
+ * @param aCSSSelector - used to specify the frame to view the source of.
+ * @returns the new tab which shows the source.
+ */
+function* openViewFrameSourceTab(aCSSSelector) {
+ let contentAreaContextMenuPopup =
+ document.getElementById("contentAreaContextMenu");
+ let popupShownPromise =
+ BrowserTestUtils.waitForEvent(contentAreaContextMenuPopup, "popupshown");
+ yield BrowserTestUtils.synthesizeMouseAtCenter(aCSSSelector,
+ { type: "contextmenu", button: 2 }, gBrowser.selectedBrowser);
+ yield popupShownPromise;
+
+ let frameContextMenu = document.getElementById("frame");
+ popupShownPromise =
+ BrowserTestUtils.waitForEvent(frameContextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(frameContextMenu, {});
+ yield popupShownPromise;
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null);
+
+ let popupHiddenPromise =
+ BrowserTestUtils.waitForEvent(frameContextMenu, "popuphidden");
+ let item = document.getElementById("context-viewframesource");
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ yield popupHiddenPromise;
+
+ return (yield newTabPromise);
+}
+
+registerCleanupFunction(function() {
+ var windows = Services.wm.getEnumerator(WINDOW_TYPE);
+ ok(!windows.hasMoreElements(), "No remaining view source windows still open");
+ while (windows.hasMoreElements())
+ windows.getNext().close();
+});
+
+/**
+ * For a given view source tab / window, wait for the source loading step to
+ * complete.
+ */
+function waitForSourceLoaded(tabOrWindow) {
+ return new Promise(resolve => {
+ let mm = tabOrWindow.messageManager ||
+ tabOrWindow.linkedBrowser.messageManager;
+ mm.addMessageListener("ViewSource:SourceLoaded", function sourceLoaded() {
+ mm.removeMessageListener("ViewSource:SourceLoaded", sourceLoaded);
+ setTimeout(resolve, 0);
+ });
+ });
+}
+
+/**
+ * Open a new document in a new tab, select part of it, and view the source of
+ * that selection. The document is not closed afterwards.
+ *
+ * @param aURI - url to load
+ * @param aCSSSelector - used to specify a node to select. All of this node's
+ * children will be selected.
+ * @returns the new tab / window which shows the source.
+ */
+function* openDocumentSelect(aURI, aCSSSelector) {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, aURI);
+ registerCleanupFunction(function() {
+ gBrowser.removeTab(tab);
+ });
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, { selector: aCSSSelector }, function* (arg) {
+ let element = content.document.querySelector(arg.selector);
+ content.getSelection().selectAllChildren(element);
+ });
+
+ let tabOrWindow = yield openViewPartialSource(aCSSSelector);
+
+ // Wait until the source has been loaded.
+ yield waitForSourceLoaded(tabOrWindow);
+
+ return tabOrWindow;
+}
+
+function pushPrefs(...aPrefs) {
+ return new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve);
+ });
+}
+
+function waitForPrefChange(pref) {
+ let deferred = PromiseUtils.defer();
+ let observer = () => {
+ Preferences.ignore(pref, observer);
+ deferred.resolve();
+ };
+ Preferences.observe(pref, observer);
+ return deferred.promise;
+}
diff --git a/toolkit/components/viewsource/test/chrome.ini b/toolkit/components/viewsource/test/chrome.ini
new file mode 100644
index 000000000..bd013ab6c
--- /dev/null
+++ b/toolkit/components/viewsource/test/chrome.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+[test_bug428653.html]
+support-files = file_empty.html
diff --git a/toolkit/components/viewsource/test/file_empty.html b/toolkit/components/viewsource/test/file_empty.html
new file mode 100644
index 000000000..495c23ec8
--- /dev/null
+++ b/toolkit/components/viewsource/test/file_empty.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><body></body></html>
diff --git a/toolkit/components/viewsource/test/test_bug428653.html b/toolkit/components/viewsource/test/test_bug428653.html
new file mode 100644
index 000000000..b1d48bfb3
--- /dev/null
+++ b/toolkit/components/viewsource/test/test_bug428653.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=428653
+-->
+<head>
+ <title>View Source Test (bug 428653)</title>
+ <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+ <iframe id="content" src="http://example.org/tests/toolkit/components/viewsource/test/file_empty.html"></iframe>
+
+ <script type="application/javascript">
+ /*
+ Test that we can't call the content browser's document.open() over Xrays.
+ See the security checks in nsHTMLDocument::Open, which make sure that the
+ entry global's principal matches that of the document.
+ */
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function testDocumentOpen() {
+ var browser = document.getElementById("content");
+ ok(browser, "got browser");
+ var doc = browser.contentDocument;
+ ok(doc, "got content document");
+
+ var opened = false;
+ try {
+ doc.open("text/html", "replace");
+ opened = true;
+ } catch (e) {
+ is(e.name, "SecurityError", "Unexpected exception")
+ }
+ is(opened, false, "Shouldn't have opened document");
+
+ doc.wrappedJSObject.open("text/html", "replace");
+ ok(true, "Should be able to open document via Xray Waiver");
+
+ SimpleTest.finish();
+ });
+ </script>
+</body>
+</html>