Bug 1820641: Make a test that calls window.restore handle zoomed windows. r=mstange
[gecko.git] / toolkit / actors / ViewSourceChild.sys.mjs
blob4c573865b76d22250cf590a9fa360016687decec
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const lazy = {};
8 ChromeUtils.defineESModuleGetters(lazy, {
9   ViewSourcePageChild: "resource://gre/actors/ViewSourcePageChild.sys.mjs",
10 });
12 export class ViewSourceChild extends JSWindowActorChild {
13   receiveMessage(message) {
14     let data = message.data;
15     switch (message.name) {
16       case "ViewSource:LoadSource":
17         this.viewSource(data.URL, data.outerWindowID, data.lineNumber);
18         break;
19       case "ViewSource:LoadSourceWithSelection":
20         this.viewSourceWithSelection(
21           data.URL,
22           data.drawSelection,
23           data.baseURI
24         );
25         break;
26       case "ViewSource:GetSelection":
27         let selectionDetails;
28         try {
29           selectionDetails = this.getSelection(this.document.ownerGlobal);
30         } catch (e) {}
31         return selectionDetails;
32     }
34     return undefined;
35   }
37   /**
38    * Called when the parent sends a message to view some source code.
39    *
40    * @param URL (required)
41    *        The URL string of the source to be shown.
42    * @param outerWindowID (optional)
43    *        The outerWindowID of the content window that has hosted
44    *        the document, in case we want to retrieve it from the network
45    *        cache.
46    * @param lineNumber (optional)
47    *        The line number to focus as soon as the source has finished
48    *        loading.
49    */
50   viewSource(URL, outerWindowID, lineNumber) {
51     let otherDocShell;
52     let forceEncodingDetection = false;
54     if (outerWindowID) {
55       let contentWindow = Services.wm.getOuterWindowWithId(outerWindowID);
56       if (contentWindow) {
57         otherDocShell = contentWindow.docShell;
59         forceEncodingDetection = contentWindow.windowUtils.docCharsetIsForced;
60       }
61     }
63     this.loadSource(URL, otherDocShell, lineNumber, forceEncodingDetection);
64   }
66   /**
67    * Loads a view source selection showing the given view-source url and
68    * highlight the selection.
69    *
70    * @param uri view-source uri to show
71    * @param drawSelection true to highlight the selection
72    * @param baseURI base URI of the original document
73    */
74   viewSourceWithSelection(uri, drawSelection, baseURI) {
75     // This isn't ideal, but set a global in the view source page actor
76     // that indicates that a selection should be drawn. It will be read
77     // when by the page's pageshow listener. This should work as the
78     // view source page is always loaded in the same process.
79     lazy.ViewSourcePageChild.setNeedsDrawSelection(drawSelection);
81     // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
82     let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
83     let webNav = this.docShell.QueryInterface(Ci.nsIWebNavigation);
84     let loadURIOptions = {
85       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
86       loadFlags,
87       baseURI: Services.io.newURI(baseURI),
88     };
89     webNav.fixupAndLoadURIString(uri, loadURIOptions);
90   }
92   /**
93    * Common utility function used by both the current and deprecated APIs
94    * for loading source.
95    *
96    * @param URL (required)
97    *        The URL string of the source to be shown.
98    * @param otherDocShell (optional)
99    *        The docshell of the content window that is hosting the document.
100    * @param lineNumber (optional)
101    *        The line number to focus as soon as the source has finished
102    *        loading.
103    * @param forceEncodingDetection (optional)
104    *        Force autodetection of the character encoding.
105    */
106   loadSource(URL, otherDocShell, lineNumber, forceEncodingDetection) {
107     const viewSrcURL = "view-source:" + URL;
109     if (forceEncodingDetection) {
110       this.docShell.forceEncodingDetection();
111     }
113     if (lineNumber) {
114       lazy.ViewSourcePageChild.setInitialLineNumber(lineNumber);
115     }
117     if (!otherDocShell) {
118       this.loadSourceFromURL(viewSrcURL);
119       return;
120     }
122     try {
123       let pageLoader = this.docShell.QueryInterface(Ci.nsIWebPageDescriptor);
124       pageLoader.loadPageAsViewSource(otherDocShell, viewSrcURL);
125     } catch (e) {
126       // We were not able to load the source from the network cache.
127       this.loadSourceFromURL(viewSrcURL);
128     }
129   }
131   /**
132    * Load some URL in the browser.
133    *
134    * @param URL
135    *        The URL string to load.
136    */
137   loadSourceFromURL(URL) {
138     let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
139     let webNav = this.docShell.QueryInterface(Ci.nsIWebNavigation);
140     let loadURIOptions = {
141       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
142       loadFlags,
143     };
144     webNav.fixupAndLoadURIString(URL, loadURIOptions);
145   }
147   /**
148    * A helper to get a path like FIXptr, but with an array instead of the
149    * "tumbler" notation.
150    * See FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
151    */
152   getPath(ancestor, node) {
153     var n = node;
154     var p = n.parentNode;
155     if (n == ancestor || !p) {
156       return null;
157     }
158     var path = [];
159     if (!path) {
160       return null;
161     }
162     do {
163       for (var i = 0; i < p.childNodes.length; i++) {
164         if (p.childNodes.item(i) == n) {
165           path.push(i);
166           break;
167         }
168       }
169       n = p;
170       p = n.parentNode;
171     } while (n != ancestor && p);
172     return path;
173   }
175   getSelection(global) {
176     const { content } = global;
178     // These are markers used to delimit the selection during processing. They
179     // are removed from the final rendering.
180     // We use noncharacter Unicode codepoints to minimize the risk of clashing
181     // with anything that might legitimately be present in the document.
182     // U+FDD0..FDEF <noncharacters>
183     const MARK_SELECTION_START = "\uFDD0";
184     const MARK_SELECTION_END = "\uFDEF";
186     var focusedWindow = Services.focus.focusedWindow || content;
187     var selection = focusedWindow.getSelection();
189     var range = selection.getRangeAt(0);
190     var ancestorContainer = range.commonAncestorContainer;
191     var doc = ancestorContainer.ownerDocument;
193     var startContainer = range.startContainer;
194     var endContainer = range.endContainer;
195     var startOffset = range.startOffset;
196     var endOffset = range.endOffset;
198     // let the ancestor be an element
199     var Node = doc.defaultView.Node;
200     if (
201       ancestorContainer.nodeType == Node.TEXT_NODE ||
202       ancestorContainer.nodeType == Node.CDATA_SECTION_NODE
203     ) {
204       ancestorContainer = ancestorContainer.parentNode;
205     }
207     // for selectAll, let's use the entire document, including <html>...</html>
208     // @see nsDocumentViewer::SelectAll() for how selectAll is implemented
209     try {
210       if (ancestorContainer == doc.body) {
211         ancestorContainer = doc.documentElement;
212       }
213     } catch (e) {}
215     // each path is a "child sequence" (a.k.a. "tumbler") that
216     // descends from the ancestor down to the boundary point
217     var startPath = this.getPath(ancestorContainer, startContainer);
218     var endPath = this.getPath(ancestorContainer, endContainer);
220     // clone the fragment of interest and reset everything to be relative to it
221     // note: it is with the clone that we operate/munge from now on.  Also note
222     // that we clone into a data document to prevent images in the fragment from
223     // loading and the like.  The use of importNode here, as opposed to adoptNode,
224     // is _very_ important.
225     // XXXbz wish there were a less hacky way to create an untrusted document here
226     var isHTML = doc.createElement("div").tagName == "DIV";
227     var dataDoc = isHTML
228       ? ancestorContainer.ownerDocument.implementation.createHTMLDocument("")
229       : ancestorContainer.ownerDocument.implementation.createDocument(
230           "",
231           "",
232           null
233         );
234     ancestorContainer = dataDoc.importNode(ancestorContainer, true);
235     startContainer = ancestorContainer;
236     endContainer = ancestorContainer;
238     // Only bother with the selection if it can be remapped. Don't mess with
239     // leaf elements (such as <isindex>) that secretly use anynomous content
240     // for their display appearance.
241     var canDrawSelection = ancestorContainer.hasChildNodes();
242     var tmpNode;
243     if (canDrawSelection) {
244       var i;
245       for (i = startPath ? startPath.length - 1 : -1; i >= 0; i--) {
246         startContainer = startContainer.childNodes.item(startPath[i]);
247       }
248       for (i = endPath ? endPath.length - 1 : -1; i >= 0; i--) {
249         endContainer = endContainer.childNodes.item(endPath[i]);
250       }
252       // add special markers to record the extent of the selection
253       // note: |startOffset| and |endOffset| are interpreted either as
254       // offsets in the text data or as child indices (see the Range spec)
255       // (here, munging the end point first to keep the start point safe...)
256       if (
257         endContainer.nodeType == Node.TEXT_NODE ||
258         endContainer.nodeType == Node.CDATA_SECTION_NODE
259       ) {
260         // do some extra tweaks to try to avoid the view-source output to look like
261         // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
262         // To get a neat output, the idea here is to remap the end point from:
263         // 1. ...<tag>]...   to   ...]<tag>...
264         // 2. ...]</tag>...  to   ...</tag>]...
265         if (
266           (endOffset > 0 && endOffset < endContainer.data.length) ||
267           !endContainer.parentNode ||
268           !endContainer.parentNode.parentNode
269         ) {
270           endContainer.insertData(endOffset, MARK_SELECTION_END);
271         } else {
272           tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
273           endContainer = endContainer.parentNode;
274           if (endOffset === 0) {
275             endContainer.parentNode.insertBefore(tmpNode, endContainer);
276           } else {
277             endContainer.parentNode.insertBefore(
278               tmpNode,
279               endContainer.nextSibling
280             );
281           }
282         }
283       } else {
284         tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
285         endContainer.insertBefore(
286           tmpNode,
287           endContainer.childNodes.item(endOffset)
288         );
289       }
291       if (
292         startContainer.nodeType == Node.TEXT_NODE ||
293         startContainer.nodeType == Node.CDATA_SECTION_NODE
294       ) {
295         // do some extra tweaks to try to avoid the view-source output to look like
296         // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
297         // To get a neat output, the idea here is to remap the start point from:
298         // 1. ...<tag>[...   to   ...[<tag>...
299         // 2. ...[</tag>...  to   ...</tag>[...
300         if (
301           (startOffset > 0 && startOffset < startContainer.data.length) ||
302           !startContainer.parentNode ||
303           !startContainer.parentNode.parentNode ||
304           startContainer != startContainer.parentNode.lastChild
305         ) {
306           startContainer.insertData(startOffset, MARK_SELECTION_START);
307         } else {
308           tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
309           startContainer = startContainer.parentNode;
310           if (startOffset === 0) {
311             startContainer.parentNode.insertBefore(tmpNode, startContainer);
312           } else {
313             startContainer.parentNode.insertBefore(
314               tmpNode,
315               startContainer.nextSibling
316             );
317           }
318         }
319       } else {
320         tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
321         startContainer.insertBefore(
322           tmpNode,
323           startContainer.childNodes.item(startOffset)
324         );
325       }
326     }
328     // now extract and display the syntax highlighted source
329     tmpNode = dataDoc.createElementNS("http://www.w3.org/1999/xhtml", "div");
330     tmpNode.appendChild(ancestorContainer);
332     return {
333       URL:
334         (isHTML
335           ? "view-source:data:text/html;charset=utf-8,"
336           : "view-source:data:application/xml;charset=utf-8,") +
337         encodeURIComponent(tmpNode.innerHTML),
338       drawSelection: canDrawSelection,
339       baseURI: doc.baseURI,
340     };
341   }
343   get wrapLongLines() {
344     return Services.prefs.getBoolPref("view_source.wrap_long_lines");
345   }