/* 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 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 const MARK_SELECTION_START = "\uFDD0"; const MARK_SELECTION_END = "\uFDEF"; /** * 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. */ let gNeedsDrawSelection = false; /** * Start at a specific line number. */ let gInitialLineNumber = -1; export class ViewSourcePageChild extends JSWindowActorChild { constructor() { super(); ChromeUtils.defineLazyGetter(this, "bundle", function () { return Services.strings.createBundle(BUNDLE_URL); }); } static setNeedsDrawSelection(value) { gNeedsDrawSelection = value; } static setInitialLineNumber(value) { gInitialLineNumber = value; } receiveMessage(msg) { switch (msg.name) { case "ViewSource:GoToLine": this.goToLine(msg.data.lineNumber); break; } return undefined; } /** * Any events should get handled here, and should get dispatched to * a specific function for the event type. */ handleEvent(event) { switch (event.type) { case "pageshow": this.onPageShow(event); break; case "click": this.onClick(event); break; } } /** * A shortcut to the nsISelectionController for the content. */ get selectionController() { return this.docShell .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsISelectionDisplay) .QueryInterface(Ci.nsISelectionController); } /** * A shortcut to the nsIWebBrowserFind for the content. */ get webBrowserFind() { return this.docShell .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebBrowserFind); } /** * 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. */ onClick(event) { let target = event.originalTarget; // 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("goBackButton")) { // Instead of loading some safe page, just close the window this.sendAsyncMessage("ViewSource:Close"); } } } /** * Handler for the pageshow event. * * @param event * The pageshow event being handled. */ onPageShow() { // If we need to draw the selection, wait until an actual view source page // has loaded, instead of about:blank. if ( gNeedsDrawSelection && this.document.documentURI.startsWith("view-source:") ) { gNeedsDrawSelection = false; this.drawSelection(); } if (gInitialLineNumber >= 0) { this.goToLine(gInitialLineNumber); gInitialLineNumber = -1; } } /** * 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 range = this.findLocation(lineNumber); if (!range) { this.sendAsyncMessage("ViewSource:GoToLine:Failed"); return; } let selection = this.document.defaultView.getSelection(); selection.removeAllRanges(); selection.addRange(range); 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 ); this.sendAsyncMessage("ViewSource:GoToLine:Success", { lineNumber }); } findLocation(lineNumber) { let line = this.document.getElementById(`line${lineNumber}`); let range = null; if (line) { range = this.document.createRange(); range.setStart(line, 0); range.setEndAfter(line, line.childNodes.length); return range; } let pre = this.document.querySelector("pre"); if (pre.id) { return null; } // Walk through each of the text nodes and count newlines. let curLine = 1; let treewalker = this.document.createTreeWalker( pre, NodeFilter.SHOW_TEXT, null ); 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 (lastLineInNode < lineNumber) { 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 (curLine == lineNumber && !range) { range = this.document.createRange(); 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). range.setEndAfter(pre.lastChild); } else if (curLine == lineNumber + 1) { range.setEnd(textNode, curPos - 1); break; } } } return range; } /** * 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() { this.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 = this.document.defaultView.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; } }