1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
7 // These are markers used to delimit the selection during processing. They
8 // are removed from the final rendering.
9 // We use noncharacter Unicode codepoints to minimize the risk of clashing
10 // with anything that might legitimately be present in the document.
11 // U+FDD0..FDEF <noncharacters>
12 const MARK_SELECTION_START = "\uFDD0";
13 const MARK_SELECTION_END = "\uFDEF";
16 * When showing selection source, chrome will construct a page fragment to
17 * show, and then instruct content to draw a selection after load. This is
18 * set true when there is a pending request to draw selection.
20 let gNeedsDrawSelection = false;
23 * Start at a specific line number.
25 let gInitialLineNumber = -1;
27 export class ViewSourcePageChild extends JSWindowActorChild {
31 ChromeUtils.defineLazyGetter(this, "bundle", function () {
32 return Services.strings.createBundle(BUNDLE_URL);
36 static setNeedsDrawSelection(value) {
37 gNeedsDrawSelection = value;
40 static setInitialLineNumber(value) {
41 gInitialLineNumber = value;
46 case "ViewSource:GoToLine":
47 this.goToLine(msg.data.lineNumber);
49 case "ViewSource:IsWrapping":
50 return this.isWrapping;
51 case "ViewSource:IsSyntaxHighlighting":
52 return this.isSyntaxHighlighting;
53 case "ViewSource:ToggleWrapping":
54 this.toggleWrapping();
56 case "ViewSource:ToggleSyntaxHighlighting":
57 this.toggleSyntaxHighlighting();
64 * Any events should get handled here, and should get dispatched to
65 * a specific function for the event type.
70 this.onPageShow(event);
79 * A shortcut to the nsISelectionController for the content.
81 get selectionController() {
83 .QueryInterface(Ci.nsIInterfaceRequestor)
84 .getInterface(Ci.nsISelectionDisplay)
85 .QueryInterface(Ci.nsISelectionController);
89 * A shortcut to the nsIWebBrowserFind for the content.
91 get webBrowserFind() {
93 .QueryInterface(Ci.nsIInterfaceRequestor)
94 .getInterface(Ci.nsIWebBrowserFind);
98 * This handler is for click events from:
99 * * error page content, which can show up if the user attempts to view the
100 * source of an attack page.
103 let target = event.originalTarget;
105 // Don't trust synthetic events
106 if (!event.isTrusted || event.target.localName != "button") {
110 let errorDoc = target.ownerDocument;
112 if (/^about:blocked/.test(errorDoc.documentURI)) {
113 // The event came from a button on a malware/phishing block page
115 if (target == errorDoc.getElementById("goBackButton")) {
116 // Instead of loading some safe page, just close the window
117 this.sendAsyncMessage("ViewSource:Close");
123 * Handler for the pageshow event.
126 * The pageshow event being handled.
129 // If we need to draw the selection, wait until an actual view source page
130 // has loaded, instead of about:blank.
132 gNeedsDrawSelection &&
133 this.document.documentURI.startsWith("view-source:")
135 gNeedsDrawSelection = false;
136 this.drawSelection();
139 if (gInitialLineNumber >= 0) {
140 this.goToLine(gInitialLineNumber);
141 gInitialLineNumber = -1;
146 * Attempts to go to a particular line in the source code being
147 * shown. If it succeeds in finding the line, it will fire a
148 * "ViewSource:GoToLine:Success" message, passing up an object
149 * with the lineNumber we just went to. If it cannot find the line,
150 * it will fire a "ViewSource:GoToLine:Failed" message.
153 * The line number to attempt to go to.
155 goToLine(lineNumber) {
156 let body = this.document.body;
158 // The source document is made up of a number of pre elements with
159 // id attributes in the format <pre id="line123">, meaning that
160 // the first line in the pre element is number 123.
161 // Do binary search to find the pre element containing the line.
162 // However, in the plain text case, we have only one pre without an
163 // attribute, so assume it begins on line 1.
165 for (let lbound = 0, ubound = body.childNodes.length; ; ) {
166 let middle = (lbound + ubound) >> 1;
167 pre = body.childNodes[middle];
169 let firstLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
171 if (lbound == ubound - 1) {
175 if (lineNumber >= firstLine) {
183 let found = this.findLocation(pre, lineNumber, null, -1, false, result);
186 this.sendAsyncMessage("ViewSource:GoToLine:Failed");
190 let selection = this.document.defaultView.getSelection();
191 selection.removeAllRanges();
193 // In our case, the range's startOffset is after "\n" on the previous line.
194 // Tune the selection at the beginning of the next line and do some tweaking
195 // to position the focusNode and the caret at the beginning of the line.
196 selection.interlinePosition = true;
198 selection.addRange(result.range);
200 if (!selection.isCollapsed) {
201 selection.collapseToEnd();
203 let offset = result.range.startOffset;
204 let node = result.range.startContainer;
205 if (offset < node.data.length) {
206 // The same text node spans across the "\n", just focus where we were.
207 selection.extend(node, offset);
209 // There is another tag just after the "\n", hook there. We need
210 // to focus a safe point because there are edgy cases such as
211 // <span>...\n</span><span>...</span> vs.
212 // <span>...\n<span>...</span></span><span>...</span>
213 node = node.nextSibling
215 : node.parentNode.nextSibling;
216 selection.extend(node, 0);
220 let selCon = this.selectionController;
221 selCon.setDisplaySelection(Ci.nsISelectionController.SELECTION_ON);
222 selCon.setCaretVisibilityDuringSelection(true);
224 // Scroll the beginning of the line into view.
225 selCon.scrollSelectionIntoView(
226 Ci.nsISelectionController.SELECTION_NORMAL,
227 Ci.nsISelectionController.SELECTION_FOCUS_REGION,
231 this.sendAsyncMessage("ViewSource:GoToLine:Success", { lineNumber });
235 * Some old code from the original view source implementation. Original
236 * documentation follows:
238 * "Loops through the text lines in the pre element. The arguments are either
239 * (pre, line) or (node, offset, interlinePosition). result is an out
240 * argument. If (pre, line) are specified (and node == null), result.range is
241 * a range spanning the specified line. If the (node, offset,
242 * interlinePosition) are specified, result.line and result.col are the line
243 * and column number of the specified offset in the specified node relative to
246 findLocation(pre, lineNumber, node, offset, interlinePosition, result) {
248 // Look upwards to find the current pre element.
249 // eslint-disable-next-line no-empty
250 for (pre = node; pre.nodeName != "PRE"; pre = pre.parentNode) {}
253 // The source document is made up of a number of pre elements with
254 // id attributes in the format <pre id="line123">, meaning that
255 // the first line in the pre element is number 123.
256 // However, in the plain text case, there is only one <pre> without an id,
258 let curLine = pre.id ? parseInt(pre.id.substring(4)) : 1;
260 // Walk through each of the text nodes and count newlines.
261 let treewalker = this.document.createTreeWalker(
263 NodeFilter.SHOW_TEXT,
267 // The column number of the first character in the current text node.
272 let textNode = treewalker.firstChild();
274 textNode = treewalker.nextNode()
276 // \r is not a valid character in the DOM, so we only check for \n.
277 let lineArray = textNode.data.split(/\n/);
278 let lastLineInNode = curLine + lineArray.length - 1;
280 // Check if we can skip the text node without further inspection.
281 if (node ? textNode != node : lastLineInNode < lineNumber) {
282 if (lineArray.length > 1) {
285 firstCol += lineArray[lineArray.length - 1].length;
286 curLine = lastLineInNode;
290 // curPos is the offset within the current text node of the first
291 // character in the current line.
293 var i = 0, curPos = 0;
294 i < lineArray.length;
295 curPos += lineArray[i++].length + 1
302 if (offset >= curPos && offset <= curPos + lineArray[i].length) {
303 // If we are right after the \n of a line and interlinePosition is
304 // false, the caret looks as if it were at the end of the previous
305 // line, so we display that line and column instead.
307 if (i > 0 && offset == curPos && !interlinePosition) {
308 result.line = curLine - 1;
309 var prevPos = curPos - lineArray[i - 1].length;
310 result.col = (i == 1 ? firstCol : 1) + offset - prevPos;
312 result.line = curLine;
313 result.col = (i == 0 ? firstCol : 1) + offset - curPos;
319 } else if (curLine == lineNumber && !("range" in result)) {
320 result.range = this.document.createRange();
321 result.range.setStart(textNode, curPos);
323 // This will always be overridden later, except when we look for
324 // the very last line in the file (this is the only line that does
326 result.range.setEndAfter(pre.lastChild);
327 } else if (curLine == lineNumber + 1) {
328 result.range.setEnd(textNode, curPos - 1);
335 return found || "range" in result;
339 * @return {boolean} whether the "wrap" class exists on the document body.
342 return this.document.body.classList.contains("wrap");
346 * @return {boolean} whether the "highlight" class exists on the document body.
348 get isSyntaxHighlighting() {
349 return this.document.body.classList.contains("highlight");
353 * Toggles the "wrap" class on the document body, which sets whether
354 * or not long lines are wrapped. Notifies parent to update the pref.
357 let body = this.document.body;
358 let state = body.classList.toggle("wrap");
359 this.sendAsyncMessage("ViewSource:StoreWrapping", { state });
363 * Toggles the "highlight" class on the document body, which sets whether
364 * or not syntax highlighting is displayed. Notifies parent to update the
367 toggleSyntaxHighlighting() {
368 let body = this.document.body;
369 let state = body.classList.toggle("highlight");
370 this.sendAsyncMessage("ViewSource:StoreSyntaxHighlighting", { state });
374 * Using special markers left in the serialized source, this helper makes the
375 * underlying markup of the selected fragment to automatically appear as
376 * selected on the inflated view-source DOM.
379 this.document.title = this.bundle.GetStringFromName(
380 "viewSelectionSourceTitle"
383 // find the special selection markers that we added earlier, and
384 // draw the selection between the two...
385 var findService = null;
387 // get the find service which stores the global find state
388 findService = Cc["@mozilla.org/find/find_service;1"].getService(
396 // cache the current global find state
397 var matchCase = findService.matchCase;
398 var entireWord = findService.entireWord;
399 var wrapFind = findService.wrapFind;
400 var findBackwards = findService.findBackwards;
401 var searchString = findService.searchString;
402 var replaceString = findService.replaceString;
404 // setup our find instance
405 var findInst = this.webBrowserFind;
406 findInst.matchCase = true;
407 findInst.entireWord = false;
408 findInst.wrapFind = true;
409 findInst.findBackwards = false;
411 // ...lookup the start mark
412 findInst.searchString = MARK_SELECTION_START;
413 var startLength = MARK_SELECTION_START.length;
416 var selection = this.document.defaultView.getSelection();
417 if (!selection.rangeCount) {
421 var range = selection.getRangeAt(0);
423 var startContainer = range.startContainer;
424 var startOffset = range.startOffset;
426 // ...lookup the end mark
427 findInst.searchString = MARK_SELECTION_END;
428 var endLength = MARK_SELECTION_END.length;
431 var endContainer = selection.anchorNode;
432 var endOffset = selection.anchorOffset;
434 // reset the selection that find has left
435 selection.removeAllRanges();
437 // delete the special markers now...
438 endContainer.deleteData(endOffset, endLength);
439 startContainer.deleteData(startOffset, startLength);
440 if (startContainer == endContainer) {
441 endOffset -= startLength;
442 } // has shrunk if on same text node...
443 range.setEnd(endContainer, endOffset);
445 // show the selection and scroll it into view
446 selection.addRange(range);
447 // the default behavior of the selection is to scroll at the end of
448 // the selection, whereas in this situation, it is more user-friendly
449 // to scroll at the beginning. So we override the default behavior here
451 this.selectionController.scrollSelectionIntoView(
452 Ci.nsISelectionController.SELECTION_NORMAL,
453 Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
458 // restore the current find state
459 findService.matchCase = matchCase;
460 findService.entireWord = entireWord;
461 findService.wrapFind = wrapFind;
462 findService.findBackwards = findBackwards;
463 findService.searchString = searchString;
464 findService.replaceString = replaceString;
466 findInst.matchCase = matchCase;
467 findInst.entireWord = entireWord;
468 findInst.wrapFind = wrapFind;
469 findInst.findBackwards = findBackwards;
470 findInst.searchString = searchString;