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