summaryrefslogtreecommitdiffstats
path: root/toolkit/components/viewsource/content/viewSource-content.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/viewsource/content/viewSource-content.js')
-rw-r--r--toolkit/components/viewsource/content/viewSource-content.js978
1 files changed, 978 insertions, 0 deletions
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();