/* 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:LoadSourceOriginal", "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:LoadSourceOriginal": this.viewSourceOriginal(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 original 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. */ viewSourceOriginal(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 original API should not be used with " + "remote browsers."); } this.loadSource(URL, pageDescriptor, lineNumber, forcedCharSet); }, /** * Common utility function used by both the current and orginal 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); let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); shEntry.triggeringPrincipal = systemPrincipal; 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();