1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/viewsource/content/viewSourceUtils.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,345 @@ 1.4 +// -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 1.5 + 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +/* 1.11 + * To keep the global namespace safe, don't define global variables and 1.12 + * functions in this file. 1.13 + * 1.14 + * This file silently depends on contentAreaUtils.js for 1.15 + * getDefaultFileName, getNormalizedLeafName and getDefaultExtension 1.16 + */ 1.17 + 1.18 +var gViewSourceUtils = { 1.19 + 1.20 + mnsIWebBrowserPersist: Components.interfaces.nsIWebBrowserPersist, 1.21 + mnsIWebProgress: Components.interfaces.nsIWebProgress, 1.22 + mnsIWebPageDescriptor: Components.interfaces.nsIWebPageDescriptor, 1.23 + 1.24 + // Opens view source 1.25 + viewSource: function(aURL, aPageDescriptor, aDocument, aLineNumber) 1.26 + { 1.27 + var prefs = Components.classes["@mozilla.org/preferences-service;1"] 1.28 + .getService(Components.interfaces.nsIPrefBranch); 1.29 + if (prefs.getBoolPref("view_source.editor.external")) 1.30 + this.openInExternalEditor(aURL, aPageDescriptor, aDocument, aLineNumber); 1.31 + else 1.32 + this.openInInternalViewer(aURL, aPageDescriptor, aDocument, aLineNumber); 1.33 + }, 1.34 + 1.35 + // Opens the interval view source viewer 1.36 + openInInternalViewer: function(aURL, aPageDescriptor, aDocument, aLineNumber) 1.37 + { 1.38 + // try to open a view-source window while inheriting the charset (if any) 1.39 + var charset = null; 1.40 + var isForcedCharset = false; 1.41 + if (aDocument) { 1.42 + charset = "charset=" + aDocument.characterSet; 1.43 + try { 1.44 + isForcedCharset = 1.45 + aDocument.defaultView 1.46 + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) 1.47 + .getInterface(Components.interfaces.nsIDOMWindowUtils) 1.48 + .docCharsetIsForced; 1.49 + } catch (ex) { 1.50 + } 1.51 + } 1.52 + openDialog("chrome://global/content/viewSource.xul", 1.53 + "_blank", 1.54 + "all,dialog=no", 1.55 + aURL, charset, aPageDescriptor, aLineNumber, isForcedCharset); 1.56 + }, 1.57 + 1.58 + buildEditorArgs: function(aPath, aLineNumber) { 1.59 + // Determine the command line arguments to pass to the editor. 1.60 + // We currently support a %LINE% placeholder which is set to the passed 1.61 + // line number (or to 0 if there's none) 1.62 + var editorArgs = []; 1.63 + var prefs = Components.classes["@mozilla.org/preferences-service;1"] 1.64 + .getService(Components.interfaces.nsIPrefBranch); 1.65 + var args = prefs.getCharPref("view_source.editor.args"); 1.66 + if (args) { 1.67 + args = args.replace("%LINE%", aLineNumber || "0"); 1.68 + // add the arguments to the array (keeping quoted strings intact) 1.69 + const argumentRE = /"([^"]+)"|(\S+)/g; 1.70 + while (argumentRE.test(args)) 1.71 + editorArgs.push(RegExp.$1 || RegExp.$2); 1.72 + } 1.73 + editorArgs.push(aPath); 1.74 + return editorArgs; 1.75 + }, 1.76 + 1.77 + // aCallBack is a function accepting two arguments - result (true=success) and a data object 1.78 + // It defaults to openInInternalViewer if undefined. 1.79 + openInExternalEditor: function(aURL, aPageDescriptor, aDocument, aLineNumber, aCallBack) 1.80 + { 1.81 + var data = {url: aURL, pageDescriptor: aPageDescriptor, doc: aDocument, 1.82 + lineNumber: aLineNumber}; 1.83 + 1.84 + try { 1.85 + var editor = this.getExternalViewSourceEditor(); 1.86 + if (!editor) { 1.87 + this.handleCallBack(aCallBack, false, data); 1.88 + return; 1.89 + } 1.90 + 1.91 + // make a uri 1.92 + var ios = Components.classes["@mozilla.org/network/io-service;1"] 1.93 + .getService(Components.interfaces.nsIIOService); 1.94 + var charset = aDocument ? aDocument.characterSet : null; 1.95 + var uri = ios.newURI(aURL, charset, null); 1.96 + data.uri = uri; 1.97 + 1.98 + var path; 1.99 + var contentType = aDocument ? aDocument.contentType : null; 1.100 + if (uri.scheme == "file") { 1.101 + // it's a local file; we can open it directly 1.102 + path = uri.QueryInterface(Components.interfaces.nsIFileURL).file.path; 1.103 + 1.104 + var editorArgs = this.buildEditorArgs(path, data.lineNumber); 1.105 + editor.runw(false, editorArgs, editorArgs.length); 1.106 + this.handleCallBack(aCallBack, true, data); 1.107 + } else { 1.108 + // set up the progress listener with what we know so far 1.109 + this.viewSourceProgressListener.editor = editor; 1.110 + this.viewSourceProgressListener.callBack = aCallBack; 1.111 + this.viewSourceProgressListener.data = data; 1.112 + if (!aPageDescriptor) { 1.113 + // without a page descriptor, loadPage has no chance of working. download the file. 1.114 + var file = this.getTemporaryFile(uri, aDocument, contentType); 1.115 + this.viewSourceProgressListener.file = file; 1.116 + 1.117 + let fromPrivateWindow = false; 1.118 + if (aDocument) { 1.119 + try { 1.120 + fromPrivateWindow = 1.121 + aDocument.defaultView 1.122 + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) 1.123 + .getInterface(Components.interfaces.nsIWebNavigation) 1.124 + .QueryInterface(Components.interfaces.nsILoadContext) 1.125 + .usePrivateBrowsing; 1.126 + } catch (e) { 1.127 + } 1.128 + } 1.129 + 1.130 + var webBrowserPersist = Components 1.131 + .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] 1.132 + .createInstance(this.mnsIWebBrowserPersist); 1.133 + // the default setting is to not decode. we need to decode. 1.134 + webBrowserPersist.persistFlags = this.mnsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES; 1.135 + webBrowserPersist.progressListener = this.viewSourceProgressListener; 1.136 + webBrowserPersist.savePrivacyAwareURI(uri, null, null, null, null, file, fromPrivateWindow); 1.137 + 1.138 + let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"] 1.139 + .getService(Components.interfaces.nsPIExternalAppLauncher); 1.140 + if (fromPrivateWindow) { 1.141 + // register the file to be deleted when possible 1.142 + helperService.deleteTemporaryPrivateFileWhenPossible(file); 1.143 + } else { 1.144 + // register the file to be deleted on app exit 1.145 + helperService.deleteTemporaryFileOnExit(file); 1.146 + } 1.147 + } else { 1.148 + // we'll use nsIWebPageDescriptor to get the source because it may 1.149 + // not have to refetch the file from the server 1.150 + // XXXbz this is so broken... This code doesn't set up this docshell 1.151 + // at all correctly; if somehow the view-source stuff managed to 1.152 + // execute script we'd be in big trouble here, I suspect. 1.153 + var webShell = Components.classes["@mozilla.org/docshell;1"].createInstance(); 1.154 + webShell.QueryInterface(Components.interfaces.nsIBaseWindow).create(); 1.155 + this.viewSourceProgressListener.webShell = webShell; 1.156 + var progress = webShell.QueryInterface(this.mnsIWebProgress); 1.157 + progress.addProgressListener(this.viewSourceProgressListener, 1.158 + this.mnsIWebProgress.NOTIFY_STATE_DOCUMENT); 1.159 + var pageLoader = webShell.QueryInterface(this.mnsIWebPageDescriptor); 1.160 + pageLoader.loadPage(aPageDescriptor, this.mnsIWebPageDescriptor.DISPLAY_AS_SOURCE); 1.161 + } 1.162 + } 1.163 + } catch (ex) { 1.164 + // we failed loading it with the external editor. 1.165 + Components.utils.reportError(ex); 1.166 + this.handleCallBack(aCallBack, false, data); 1.167 + return; 1.168 + } 1.169 + }, 1.170 + 1.171 + // Default callback - opens the internal viewer if the external editor failed 1.172 + internalViewerFallback: function(result, data) 1.173 + { 1.174 + if (!result) { 1.175 + this.openInInternalViewer(data.url, data.pageDescriptor, data.doc, data.lineNumber); 1.176 + } 1.177 + }, 1.178 + 1.179 + // Calls the callback, keeping in mind undefined or null values. 1.180 + handleCallBack: function(aCallBack, result, data) 1.181 + { 1.182 + // ifcallback is undefined, default to the internal viewer 1.183 + if (aCallBack === undefined) { 1.184 + this.internalViewerFallback(result, data); 1.185 + } else if (aCallBack) { 1.186 + aCallBack(result, data); 1.187 + } 1.188 + }, 1.189 + 1.190 + // Returns nsIProcess of the external view source editor or null 1.191 + getExternalViewSourceEditor: function() 1.192 + { 1.193 + try { 1.194 + let viewSourceAppPath = 1.195 + Components.classes["@mozilla.org/preferences-service;1"] 1.196 + .getService(Components.interfaces.nsIPrefBranch) 1.197 + .getComplexValue("view_source.editor.path", 1.198 + Components.interfaces.nsIFile); 1.199 + let editor = Components.classes['@mozilla.org/process/util;1'] 1.200 + .createInstance(Components.interfaces.nsIProcess); 1.201 + editor.init(viewSourceAppPath); 1.202 + 1.203 + return editor; 1.204 + } 1.205 + catch (ex) { 1.206 + Components.utils.reportError(ex); 1.207 + } 1.208 + 1.209 + return null; 1.210 + }, 1.211 + 1.212 + viewSourceProgressListener: { 1.213 + 1.214 + mnsIWebProgressListener: Components.interfaces.nsIWebProgressListener, 1.215 + 1.216 + QueryInterface: function(aIID) { 1.217 + if (aIID.equals(this.mnsIWebProgressListener) || 1.218 + aIID.equals(Components.interfaces.nsISupportsWeakReference) || 1.219 + aIID.equals(Components.interfaces.nsISupports)) 1.220 + return this; 1.221 + throw Components.results.NS_NOINTERFACE; 1.222 + }, 1.223 + 1.224 + destroy: function() { 1.225 + if (this.webShell) { 1.226 + this.webShell.QueryInterface(Components.interfaces.nsIBaseWindow).destroy(); 1.227 + } 1.228 + this.webShell = null; 1.229 + this.editor = null; 1.230 + this.callBack = null; 1.231 + this.data = null; 1.232 + this.file = null; 1.233 + }, 1.234 + 1.235 + // This listener is used both for tracking the progress of an HTML parse 1.236 + // in one case and for tracking the progress of nsIWebBrowserPersist in 1.237 + // another case. 1.238 + onStateChange: function(aProgress, aRequest, aFlag, aStatus) { 1.239 + // once it's done loading... 1.240 + if ((aFlag & this.mnsIWebProgressListener.STATE_STOP) && aStatus == 0) { 1.241 + if (!this.webShell) { 1.242 + // We aren't waiting for the parser. Instead, we are waiting for 1.243 + // an nsIWebBrowserPersist. 1.244 + this.onContentLoaded(); 1.245 + return 0; 1.246 + } 1.247 + var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation); 1.248 + if (webNavigation.document.readyState == "complete") { 1.249 + // This branch is probably never taken. Including it for completeness. 1.250 + this.onContentLoaded(); 1.251 + } else { 1.252 + webNavigation.document.addEventListener("DOMContentLoaded", 1.253 + this.onContentLoaded.bind(this)); 1.254 + } 1.255 + } 1.256 + return 0; 1.257 + }, 1.258 + 1.259 + onContentLoaded: function() { 1.260 + try { 1.261 + if (!this.file) { 1.262 + // it's not saved to file yet, it's in the webshell 1.263 + 1.264 + // get a temporary filename using the attributes from the data object that 1.265 + // openInExternalEditor gave us 1.266 + this.file = gViewSourceUtils.getTemporaryFile(this.data.uri, this.data.doc, 1.267 + this.data.doc.contentType); 1.268 + 1.269 + // we have to convert from the source charset. 1.270 + var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation); 1.271 + var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"] 1.272 + .createInstance(Components.interfaces.nsIFileOutputStream); 1.273 + foStream.init(this.file, 0x02 | 0x08 | 0x20, -1, 0); // write | create | truncate 1.274 + var coStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"] 1.275 + .createInstance(Components.interfaces.nsIConverterOutputStream); 1.276 + coStream.init(foStream, this.data.doc.characterSet, 0, null); 1.277 + 1.278 + // write the source to the file 1.279 + coStream.writeString(webNavigation.document.body.textContent); 1.280 + 1.281 + // clean up 1.282 + coStream.close(); 1.283 + foStream.close(); 1.284 + 1.285 + let fromPrivateWindow = 1.286 + this.data.doc.defaultView 1.287 + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) 1.288 + .getInterface(Components.interfaces.nsIWebNavigation) 1.289 + .QueryInterface(Components.interfaces.nsILoadContext) 1.290 + .usePrivateBrowsing; 1.291 + 1.292 + let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"] 1.293 + .getService(Components.interfaces.nsPIExternalAppLauncher); 1.294 + if (fromPrivateWindow) { 1.295 + // register the file to be deleted when possible 1.296 + helperService.deleteTemporaryPrivateFileWhenPossible(this.file); 1.297 + } else { 1.298 + // register the file to be deleted on app exit 1.299 + helperService.deleteTemporaryFileOnExit(this.file); 1.300 + } 1.301 + } 1.302 + 1.303 + var editorArgs = gViewSourceUtils.buildEditorArgs(this.file.path, 1.304 + this.data.lineNumber); 1.305 + this.editor.runw(false, editorArgs, editorArgs.length); 1.306 + 1.307 + gViewSourceUtils.handleCallBack(this.callBack, true, this.data); 1.308 + } catch (ex) { 1.309 + // we failed loading it with the external editor. 1.310 + Components.utils.reportError(ex); 1.311 + gViewSourceUtils.handleCallBack(this.callBack, false, this.data); 1.312 + } finally { 1.313 + this.destroy(); 1.314 + } 1.315 + }, 1.316 + 1.317 + onLocationChange: function() {return 0;}, 1.318 + onProgressChange: function() {return 0;}, 1.319 + onStatusChange: function() {return 0;}, 1.320 + onSecurityChange: function() {return 0;}, 1.321 + 1.322 + webShell: null, 1.323 + editor: null, 1.324 + callBack: null, 1.325 + data: null, 1.326 + file: null 1.327 + }, 1.328 + 1.329 + // returns an nsIFile for the passed document in the system temp directory 1.330 + getTemporaryFile: function(aURI, aDocument, aContentType) { 1.331 + // include contentAreaUtils.js in our own context when we first need it 1.332 + if (!this._caUtils) { 1.333 + var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] 1.334 + .getService(Components.interfaces.mozIJSSubScriptLoader); 1.335 + this._caUtils = {}; 1.336 + scriptLoader.loadSubScript("chrome://global/content/contentAreaUtils.js", this._caUtils); 1.337 + } 1.338 + 1.339 + var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"] 1.340 + .getService(Components.interfaces.nsIProperties); 1.341 + var tempFile = fileLocator.get("TmpD", Components.interfaces.nsIFile); 1.342 + var fileName = this._caUtils.getDefaultFileName(null, aURI, aDocument, aContentType); 1.343 + var extension = this._caUtils.getDefaultExtension(fileName, aURI, aContentType); 1.344 + var leafName = this._caUtils.getNormalizedLeafName(fileName, extension); 1.345 + tempFile.append(leafName); 1.346 + return tempFile; 1.347 + } 1.348 +}