|
1 // -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- |
|
2 |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 /* |
|
8 * To keep the global namespace safe, don't define global variables and |
|
9 * functions in this file. |
|
10 * |
|
11 * This file silently depends on contentAreaUtils.js for |
|
12 * getDefaultFileName, getNormalizedLeafName and getDefaultExtension |
|
13 */ |
|
14 |
|
15 var gViewSourceUtils = { |
|
16 |
|
17 mnsIWebBrowserPersist: Components.interfaces.nsIWebBrowserPersist, |
|
18 mnsIWebProgress: Components.interfaces.nsIWebProgress, |
|
19 mnsIWebPageDescriptor: Components.interfaces.nsIWebPageDescriptor, |
|
20 |
|
21 // Opens view source |
|
22 viewSource: function(aURL, aPageDescriptor, aDocument, aLineNumber) |
|
23 { |
|
24 var prefs = Components.classes["@mozilla.org/preferences-service;1"] |
|
25 .getService(Components.interfaces.nsIPrefBranch); |
|
26 if (prefs.getBoolPref("view_source.editor.external")) |
|
27 this.openInExternalEditor(aURL, aPageDescriptor, aDocument, aLineNumber); |
|
28 else |
|
29 this.openInInternalViewer(aURL, aPageDescriptor, aDocument, aLineNumber); |
|
30 }, |
|
31 |
|
32 // Opens the interval view source viewer |
|
33 openInInternalViewer: function(aURL, aPageDescriptor, aDocument, aLineNumber) |
|
34 { |
|
35 // try to open a view-source window while inheriting the charset (if any) |
|
36 var charset = null; |
|
37 var isForcedCharset = false; |
|
38 if (aDocument) { |
|
39 charset = "charset=" + aDocument.characterSet; |
|
40 try { |
|
41 isForcedCharset = |
|
42 aDocument.defaultView |
|
43 .QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
|
44 .getInterface(Components.interfaces.nsIDOMWindowUtils) |
|
45 .docCharsetIsForced; |
|
46 } catch (ex) { |
|
47 } |
|
48 } |
|
49 openDialog("chrome://global/content/viewSource.xul", |
|
50 "_blank", |
|
51 "all,dialog=no", |
|
52 aURL, charset, aPageDescriptor, aLineNumber, isForcedCharset); |
|
53 }, |
|
54 |
|
55 buildEditorArgs: function(aPath, aLineNumber) { |
|
56 // Determine the command line arguments to pass to the editor. |
|
57 // We currently support a %LINE% placeholder which is set to the passed |
|
58 // line number (or to 0 if there's none) |
|
59 var editorArgs = []; |
|
60 var prefs = Components.classes["@mozilla.org/preferences-service;1"] |
|
61 .getService(Components.interfaces.nsIPrefBranch); |
|
62 var args = prefs.getCharPref("view_source.editor.args"); |
|
63 if (args) { |
|
64 args = args.replace("%LINE%", aLineNumber || "0"); |
|
65 // add the arguments to the array (keeping quoted strings intact) |
|
66 const argumentRE = /"([^"]+)"|(\S+)/g; |
|
67 while (argumentRE.test(args)) |
|
68 editorArgs.push(RegExp.$1 || RegExp.$2); |
|
69 } |
|
70 editorArgs.push(aPath); |
|
71 return editorArgs; |
|
72 }, |
|
73 |
|
74 // aCallBack is a function accepting two arguments - result (true=success) and a data object |
|
75 // It defaults to openInInternalViewer if undefined. |
|
76 openInExternalEditor: function(aURL, aPageDescriptor, aDocument, aLineNumber, aCallBack) |
|
77 { |
|
78 var data = {url: aURL, pageDescriptor: aPageDescriptor, doc: aDocument, |
|
79 lineNumber: aLineNumber}; |
|
80 |
|
81 try { |
|
82 var editor = this.getExternalViewSourceEditor(); |
|
83 if (!editor) { |
|
84 this.handleCallBack(aCallBack, false, data); |
|
85 return; |
|
86 } |
|
87 |
|
88 // make a uri |
|
89 var ios = Components.classes["@mozilla.org/network/io-service;1"] |
|
90 .getService(Components.interfaces.nsIIOService); |
|
91 var charset = aDocument ? aDocument.characterSet : null; |
|
92 var uri = ios.newURI(aURL, charset, null); |
|
93 data.uri = uri; |
|
94 |
|
95 var path; |
|
96 var contentType = aDocument ? aDocument.contentType : null; |
|
97 if (uri.scheme == "file") { |
|
98 // it's a local file; we can open it directly |
|
99 path = uri.QueryInterface(Components.interfaces.nsIFileURL).file.path; |
|
100 |
|
101 var editorArgs = this.buildEditorArgs(path, data.lineNumber); |
|
102 editor.runw(false, editorArgs, editorArgs.length); |
|
103 this.handleCallBack(aCallBack, true, data); |
|
104 } else { |
|
105 // set up the progress listener with what we know so far |
|
106 this.viewSourceProgressListener.editor = editor; |
|
107 this.viewSourceProgressListener.callBack = aCallBack; |
|
108 this.viewSourceProgressListener.data = data; |
|
109 if (!aPageDescriptor) { |
|
110 // without a page descriptor, loadPage has no chance of working. download the file. |
|
111 var file = this.getTemporaryFile(uri, aDocument, contentType); |
|
112 this.viewSourceProgressListener.file = file; |
|
113 |
|
114 let fromPrivateWindow = false; |
|
115 if (aDocument) { |
|
116 try { |
|
117 fromPrivateWindow = |
|
118 aDocument.defaultView |
|
119 .QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
|
120 .getInterface(Components.interfaces.nsIWebNavigation) |
|
121 .QueryInterface(Components.interfaces.nsILoadContext) |
|
122 .usePrivateBrowsing; |
|
123 } catch (e) { |
|
124 } |
|
125 } |
|
126 |
|
127 var webBrowserPersist = Components |
|
128 .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] |
|
129 .createInstance(this.mnsIWebBrowserPersist); |
|
130 // the default setting is to not decode. we need to decode. |
|
131 webBrowserPersist.persistFlags = this.mnsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES; |
|
132 webBrowserPersist.progressListener = this.viewSourceProgressListener; |
|
133 webBrowserPersist.savePrivacyAwareURI(uri, null, null, null, null, file, fromPrivateWindow); |
|
134 |
|
135 let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"] |
|
136 .getService(Components.interfaces.nsPIExternalAppLauncher); |
|
137 if (fromPrivateWindow) { |
|
138 // register the file to be deleted when possible |
|
139 helperService.deleteTemporaryPrivateFileWhenPossible(file); |
|
140 } else { |
|
141 // register the file to be deleted on app exit |
|
142 helperService.deleteTemporaryFileOnExit(file); |
|
143 } |
|
144 } else { |
|
145 // we'll use nsIWebPageDescriptor to get the source because it may |
|
146 // not have to refetch the file from the server |
|
147 // XXXbz this is so broken... This code doesn't set up this docshell |
|
148 // at all correctly; if somehow the view-source stuff managed to |
|
149 // execute script we'd be in big trouble here, I suspect. |
|
150 var webShell = Components.classes["@mozilla.org/docshell;1"].createInstance(); |
|
151 webShell.QueryInterface(Components.interfaces.nsIBaseWindow).create(); |
|
152 this.viewSourceProgressListener.webShell = webShell; |
|
153 var progress = webShell.QueryInterface(this.mnsIWebProgress); |
|
154 progress.addProgressListener(this.viewSourceProgressListener, |
|
155 this.mnsIWebProgress.NOTIFY_STATE_DOCUMENT); |
|
156 var pageLoader = webShell.QueryInterface(this.mnsIWebPageDescriptor); |
|
157 pageLoader.loadPage(aPageDescriptor, this.mnsIWebPageDescriptor.DISPLAY_AS_SOURCE); |
|
158 } |
|
159 } |
|
160 } catch (ex) { |
|
161 // we failed loading it with the external editor. |
|
162 Components.utils.reportError(ex); |
|
163 this.handleCallBack(aCallBack, false, data); |
|
164 return; |
|
165 } |
|
166 }, |
|
167 |
|
168 // Default callback - opens the internal viewer if the external editor failed |
|
169 internalViewerFallback: function(result, data) |
|
170 { |
|
171 if (!result) { |
|
172 this.openInInternalViewer(data.url, data.pageDescriptor, data.doc, data.lineNumber); |
|
173 } |
|
174 }, |
|
175 |
|
176 // Calls the callback, keeping in mind undefined or null values. |
|
177 handleCallBack: function(aCallBack, result, data) |
|
178 { |
|
179 // ifcallback is undefined, default to the internal viewer |
|
180 if (aCallBack === undefined) { |
|
181 this.internalViewerFallback(result, data); |
|
182 } else if (aCallBack) { |
|
183 aCallBack(result, data); |
|
184 } |
|
185 }, |
|
186 |
|
187 // Returns nsIProcess of the external view source editor or null |
|
188 getExternalViewSourceEditor: function() |
|
189 { |
|
190 try { |
|
191 let viewSourceAppPath = |
|
192 Components.classes["@mozilla.org/preferences-service;1"] |
|
193 .getService(Components.interfaces.nsIPrefBranch) |
|
194 .getComplexValue("view_source.editor.path", |
|
195 Components.interfaces.nsIFile); |
|
196 let editor = Components.classes['@mozilla.org/process/util;1'] |
|
197 .createInstance(Components.interfaces.nsIProcess); |
|
198 editor.init(viewSourceAppPath); |
|
199 |
|
200 return editor; |
|
201 } |
|
202 catch (ex) { |
|
203 Components.utils.reportError(ex); |
|
204 } |
|
205 |
|
206 return null; |
|
207 }, |
|
208 |
|
209 viewSourceProgressListener: { |
|
210 |
|
211 mnsIWebProgressListener: Components.interfaces.nsIWebProgressListener, |
|
212 |
|
213 QueryInterface: function(aIID) { |
|
214 if (aIID.equals(this.mnsIWebProgressListener) || |
|
215 aIID.equals(Components.interfaces.nsISupportsWeakReference) || |
|
216 aIID.equals(Components.interfaces.nsISupports)) |
|
217 return this; |
|
218 throw Components.results.NS_NOINTERFACE; |
|
219 }, |
|
220 |
|
221 destroy: function() { |
|
222 if (this.webShell) { |
|
223 this.webShell.QueryInterface(Components.interfaces.nsIBaseWindow).destroy(); |
|
224 } |
|
225 this.webShell = null; |
|
226 this.editor = null; |
|
227 this.callBack = null; |
|
228 this.data = null; |
|
229 this.file = null; |
|
230 }, |
|
231 |
|
232 // This listener is used both for tracking the progress of an HTML parse |
|
233 // in one case and for tracking the progress of nsIWebBrowserPersist in |
|
234 // another case. |
|
235 onStateChange: function(aProgress, aRequest, aFlag, aStatus) { |
|
236 // once it's done loading... |
|
237 if ((aFlag & this.mnsIWebProgressListener.STATE_STOP) && aStatus == 0) { |
|
238 if (!this.webShell) { |
|
239 // We aren't waiting for the parser. Instead, we are waiting for |
|
240 // an nsIWebBrowserPersist. |
|
241 this.onContentLoaded(); |
|
242 return 0; |
|
243 } |
|
244 var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation); |
|
245 if (webNavigation.document.readyState == "complete") { |
|
246 // This branch is probably never taken. Including it for completeness. |
|
247 this.onContentLoaded(); |
|
248 } else { |
|
249 webNavigation.document.addEventListener("DOMContentLoaded", |
|
250 this.onContentLoaded.bind(this)); |
|
251 } |
|
252 } |
|
253 return 0; |
|
254 }, |
|
255 |
|
256 onContentLoaded: function() { |
|
257 try { |
|
258 if (!this.file) { |
|
259 // it's not saved to file yet, it's in the webshell |
|
260 |
|
261 // get a temporary filename using the attributes from the data object that |
|
262 // openInExternalEditor gave us |
|
263 this.file = gViewSourceUtils.getTemporaryFile(this.data.uri, this.data.doc, |
|
264 this.data.doc.contentType); |
|
265 |
|
266 // we have to convert from the source charset. |
|
267 var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation); |
|
268 var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"] |
|
269 .createInstance(Components.interfaces.nsIFileOutputStream); |
|
270 foStream.init(this.file, 0x02 | 0x08 | 0x20, -1, 0); // write | create | truncate |
|
271 var coStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"] |
|
272 .createInstance(Components.interfaces.nsIConverterOutputStream); |
|
273 coStream.init(foStream, this.data.doc.characterSet, 0, null); |
|
274 |
|
275 // write the source to the file |
|
276 coStream.writeString(webNavigation.document.body.textContent); |
|
277 |
|
278 // clean up |
|
279 coStream.close(); |
|
280 foStream.close(); |
|
281 |
|
282 let fromPrivateWindow = |
|
283 this.data.doc.defaultView |
|
284 .QueryInterface(Components.interfaces.nsIInterfaceRequestor) |
|
285 .getInterface(Components.interfaces.nsIWebNavigation) |
|
286 .QueryInterface(Components.interfaces.nsILoadContext) |
|
287 .usePrivateBrowsing; |
|
288 |
|
289 let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"] |
|
290 .getService(Components.interfaces.nsPIExternalAppLauncher); |
|
291 if (fromPrivateWindow) { |
|
292 // register the file to be deleted when possible |
|
293 helperService.deleteTemporaryPrivateFileWhenPossible(this.file); |
|
294 } else { |
|
295 // register the file to be deleted on app exit |
|
296 helperService.deleteTemporaryFileOnExit(this.file); |
|
297 } |
|
298 } |
|
299 |
|
300 var editorArgs = gViewSourceUtils.buildEditorArgs(this.file.path, |
|
301 this.data.lineNumber); |
|
302 this.editor.runw(false, editorArgs, editorArgs.length); |
|
303 |
|
304 gViewSourceUtils.handleCallBack(this.callBack, true, this.data); |
|
305 } catch (ex) { |
|
306 // we failed loading it with the external editor. |
|
307 Components.utils.reportError(ex); |
|
308 gViewSourceUtils.handleCallBack(this.callBack, false, this.data); |
|
309 } finally { |
|
310 this.destroy(); |
|
311 } |
|
312 }, |
|
313 |
|
314 onLocationChange: function() {return 0;}, |
|
315 onProgressChange: function() {return 0;}, |
|
316 onStatusChange: function() {return 0;}, |
|
317 onSecurityChange: function() {return 0;}, |
|
318 |
|
319 webShell: null, |
|
320 editor: null, |
|
321 callBack: null, |
|
322 data: null, |
|
323 file: null |
|
324 }, |
|
325 |
|
326 // returns an nsIFile for the passed document in the system temp directory |
|
327 getTemporaryFile: function(aURI, aDocument, aContentType) { |
|
328 // include contentAreaUtils.js in our own context when we first need it |
|
329 if (!this._caUtils) { |
|
330 var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] |
|
331 .getService(Components.interfaces.mozIJSSubScriptLoader); |
|
332 this._caUtils = {}; |
|
333 scriptLoader.loadSubScript("chrome://global/content/contentAreaUtils.js", this._caUtils); |
|
334 } |
|
335 |
|
336 var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"] |
|
337 .getService(Components.interfaces.nsIProperties); |
|
338 var tempFile = fileLocator.get("TmpD", Components.interfaces.nsIFile); |
|
339 var fileName = this._caUtils.getDefaultFileName(null, aURI, aDocument, aContentType); |
|
340 var extension = this._caUtils.getDefaultExtension(fileName, aURI, aContentType); |
|
341 var leafName = this._caUtils.getNormalizedLeafName(fileName, extension); |
|
342 tempFile.append(leafName); |
|
343 return tempFile; |
|
344 } |
|
345 } |