// -*- 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 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 . 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 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 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) { // 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 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")); };