diff options
Diffstat (limited to 'toolkit/components/viewsource')
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="©LinkCmd.label;" + accesskey="©LinkCmd.accesskey;" + oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/> + <menuitem id="context-copyEmail" + label="©EmailCmd.label;" + accesskey="©EmailCmd.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="©LinkCmd.label;" + accesskey="©LinkCmd.accesskey;" + oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/> + <menuitem id="context-copyEmail" + label="©EmailCmd.label;" + accesskey="©EmailCmd.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> |