michael@0: // -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- michael@0: michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * To keep the global namespace safe, don't define global variables and michael@0: * functions in this file. michael@0: * michael@0: * This file silently depends on contentAreaUtils.js for michael@0: * getDefaultFileName, getNormalizedLeafName and getDefaultExtension michael@0: */ michael@0: michael@0: var gViewSourceUtils = { michael@0: michael@0: mnsIWebBrowserPersist: Components.interfaces.nsIWebBrowserPersist, michael@0: mnsIWebProgress: Components.interfaces.nsIWebProgress, michael@0: mnsIWebPageDescriptor: Components.interfaces.nsIWebPageDescriptor, michael@0: michael@0: // Opens view source michael@0: viewSource: function(aURL, aPageDescriptor, aDocument, aLineNumber) michael@0: { michael@0: var prefs = Components.classes["@mozilla.org/preferences-service;1"] michael@0: .getService(Components.interfaces.nsIPrefBranch); michael@0: if (prefs.getBoolPref("view_source.editor.external")) michael@0: this.openInExternalEditor(aURL, aPageDescriptor, aDocument, aLineNumber); michael@0: else michael@0: this.openInInternalViewer(aURL, aPageDescriptor, aDocument, aLineNumber); michael@0: }, michael@0: michael@0: // Opens the interval view source viewer michael@0: openInInternalViewer: function(aURL, aPageDescriptor, aDocument, aLineNumber) michael@0: { michael@0: // try to open a view-source window while inheriting the charset (if any) michael@0: var charset = null; michael@0: var isForcedCharset = false; michael@0: if (aDocument) { michael@0: charset = "charset=" + aDocument.characterSet; michael@0: try { michael@0: isForcedCharset = michael@0: aDocument.defaultView michael@0: .QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIDOMWindowUtils) michael@0: .docCharsetIsForced; michael@0: } catch (ex) { michael@0: } michael@0: } michael@0: openDialog("chrome://global/content/viewSource.xul", michael@0: "_blank", michael@0: "all,dialog=no", michael@0: aURL, charset, aPageDescriptor, aLineNumber, isForcedCharset); michael@0: }, michael@0: michael@0: buildEditorArgs: function(aPath, aLineNumber) { michael@0: // Determine the command line arguments to pass to the editor. michael@0: // We currently support a %LINE% placeholder which is set to the passed michael@0: // line number (or to 0 if there's none) michael@0: var editorArgs = []; michael@0: var prefs = Components.classes["@mozilla.org/preferences-service;1"] michael@0: .getService(Components.interfaces.nsIPrefBranch); michael@0: var args = prefs.getCharPref("view_source.editor.args"); michael@0: if (args) { michael@0: args = args.replace("%LINE%", aLineNumber || "0"); michael@0: // add the arguments to the array (keeping quoted strings intact) michael@0: const argumentRE = /"([^"]+)"|(\S+)/g; michael@0: while (argumentRE.test(args)) michael@0: editorArgs.push(RegExp.$1 || RegExp.$2); michael@0: } michael@0: editorArgs.push(aPath); michael@0: return editorArgs; michael@0: }, michael@0: michael@0: // aCallBack is a function accepting two arguments - result (true=success) and a data object michael@0: // It defaults to openInInternalViewer if undefined. michael@0: openInExternalEditor: function(aURL, aPageDescriptor, aDocument, aLineNumber, aCallBack) michael@0: { michael@0: var data = {url: aURL, pageDescriptor: aPageDescriptor, doc: aDocument, michael@0: lineNumber: aLineNumber}; michael@0: michael@0: try { michael@0: var editor = this.getExternalViewSourceEditor(); michael@0: if (!editor) { michael@0: this.handleCallBack(aCallBack, false, data); michael@0: return; michael@0: } michael@0: michael@0: // make a uri michael@0: var ios = Components.classes["@mozilla.org/network/io-service;1"] michael@0: .getService(Components.interfaces.nsIIOService); michael@0: var charset = aDocument ? aDocument.characterSet : null; michael@0: var uri = ios.newURI(aURL, charset, null); michael@0: data.uri = uri; michael@0: michael@0: var path; michael@0: var contentType = aDocument ? aDocument.contentType : null; michael@0: if (uri.scheme == "file") { michael@0: // it's a local file; we can open it directly michael@0: path = uri.QueryInterface(Components.interfaces.nsIFileURL).file.path; michael@0: michael@0: var editorArgs = this.buildEditorArgs(path, data.lineNumber); michael@0: editor.runw(false, editorArgs, editorArgs.length); michael@0: this.handleCallBack(aCallBack, true, data); michael@0: } else { michael@0: // set up the progress listener with what we know so far michael@0: this.viewSourceProgressListener.editor = editor; michael@0: this.viewSourceProgressListener.callBack = aCallBack; michael@0: this.viewSourceProgressListener.data = data; michael@0: if (!aPageDescriptor) { michael@0: // without a page descriptor, loadPage has no chance of working. download the file. michael@0: var file = this.getTemporaryFile(uri, aDocument, contentType); michael@0: this.viewSourceProgressListener.file = file; michael@0: michael@0: let fromPrivateWindow = false; michael@0: if (aDocument) { michael@0: try { michael@0: fromPrivateWindow = michael@0: aDocument.defaultView michael@0: .QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIWebNavigation) michael@0: .QueryInterface(Components.interfaces.nsILoadContext) michael@0: .usePrivateBrowsing; michael@0: } catch (e) { michael@0: } michael@0: } michael@0: michael@0: var webBrowserPersist = Components michael@0: .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] michael@0: .createInstance(this.mnsIWebBrowserPersist); michael@0: // the default setting is to not decode. we need to decode. michael@0: webBrowserPersist.persistFlags = this.mnsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES; michael@0: webBrowserPersist.progressListener = this.viewSourceProgressListener; michael@0: webBrowserPersist.savePrivacyAwareURI(uri, null, null, null, null, file, fromPrivateWindow); michael@0: michael@0: let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"] michael@0: .getService(Components.interfaces.nsPIExternalAppLauncher); michael@0: if (fromPrivateWindow) { michael@0: // register the file to be deleted when possible michael@0: helperService.deleteTemporaryPrivateFileWhenPossible(file); michael@0: } else { michael@0: // register the file to be deleted on app exit michael@0: helperService.deleteTemporaryFileOnExit(file); michael@0: } michael@0: } else { michael@0: // we'll use nsIWebPageDescriptor to get the source because it may michael@0: // not have to refetch the file from the server michael@0: // XXXbz this is so broken... This code doesn't set up this docshell michael@0: // at all correctly; if somehow the view-source stuff managed to michael@0: // execute script we'd be in big trouble here, I suspect. michael@0: var webShell = Components.classes["@mozilla.org/docshell;1"].createInstance(); michael@0: webShell.QueryInterface(Components.interfaces.nsIBaseWindow).create(); michael@0: this.viewSourceProgressListener.webShell = webShell; michael@0: var progress = webShell.QueryInterface(this.mnsIWebProgress); michael@0: progress.addProgressListener(this.viewSourceProgressListener, michael@0: this.mnsIWebProgress.NOTIFY_STATE_DOCUMENT); michael@0: var pageLoader = webShell.QueryInterface(this.mnsIWebPageDescriptor); michael@0: pageLoader.loadPage(aPageDescriptor, this.mnsIWebPageDescriptor.DISPLAY_AS_SOURCE); michael@0: } michael@0: } michael@0: } catch (ex) { michael@0: // we failed loading it with the external editor. michael@0: Components.utils.reportError(ex); michael@0: this.handleCallBack(aCallBack, false, data); michael@0: return; michael@0: } michael@0: }, michael@0: michael@0: // Default callback - opens the internal viewer if the external editor failed michael@0: internalViewerFallback: function(result, data) michael@0: { michael@0: if (!result) { michael@0: this.openInInternalViewer(data.url, data.pageDescriptor, data.doc, data.lineNumber); michael@0: } michael@0: }, michael@0: michael@0: // Calls the callback, keeping in mind undefined or null values. michael@0: handleCallBack: function(aCallBack, result, data) michael@0: { michael@0: // ifcallback is undefined, default to the internal viewer michael@0: if (aCallBack === undefined) { michael@0: this.internalViewerFallback(result, data); michael@0: } else if (aCallBack) { michael@0: aCallBack(result, data); michael@0: } michael@0: }, michael@0: michael@0: // Returns nsIProcess of the external view source editor or null michael@0: getExternalViewSourceEditor: function() michael@0: { michael@0: try { michael@0: let viewSourceAppPath = michael@0: Components.classes["@mozilla.org/preferences-service;1"] michael@0: .getService(Components.interfaces.nsIPrefBranch) michael@0: .getComplexValue("view_source.editor.path", michael@0: Components.interfaces.nsIFile); michael@0: let editor = Components.classes['@mozilla.org/process/util;1'] michael@0: .createInstance(Components.interfaces.nsIProcess); michael@0: editor.init(viewSourceAppPath); michael@0: michael@0: return editor; michael@0: } michael@0: catch (ex) { michael@0: Components.utils.reportError(ex); michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: viewSourceProgressListener: { michael@0: michael@0: mnsIWebProgressListener: Components.interfaces.nsIWebProgressListener, michael@0: michael@0: QueryInterface: function(aIID) { michael@0: if (aIID.equals(this.mnsIWebProgressListener) || michael@0: aIID.equals(Components.interfaces.nsISupportsWeakReference) || michael@0: aIID.equals(Components.interfaces.nsISupports)) michael@0: return this; michael@0: throw Components.results.NS_NOINTERFACE; michael@0: }, michael@0: michael@0: destroy: function() { michael@0: if (this.webShell) { michael@0: this.webShell.QueryInterface(Components.interfaces.nsIBaseWindow).destroy(); michael@0: } michael@0: this.webShell = null; michael@0: this.editor = null; michael@0: this.callBack = null; michael@0: this.data = null; michael@0: this.file = null; michael@0: }, michael@0: michael@0: // This listener is used both for tracking the progress of an HTML parse michael@0: // in one case and for tracking the progress of nsIWebBrowserPersist in michael@0: // another case. michael@0: onStateChange: function(aProgress, aRequest, aFlag, aStatus) { michael@0: // once it's done loading... michael@0: if ((aFlag & this.mnsIWebProgressListener.STATE_STOP) && aStatus == 0) { michael@0: if (!this.webShell) { michael@0: // We aren't waiting for the parser. Instead, we are waiting for michael@0: // an nsIWebBrowserPersist. michael@0: this.onContentLoaded(); michael@0: return 0; michael@0: } michael@0: var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation); michael@0: if (webNavigation.document.readyState == "complete") { michael@0: // This branch is probably never taken. Including it for completeness. michael@0: this.onContentLoaded(); michael@0: } else { michael@0: webNavigation.document.addEventListener("DOMContentLoaded", michael@0: this.onContentLoaded.bind(this)); michael@0: } michael@0: } michael@0: return 0; michael@0: }, michael@0: michael@0: onContentLoaded: function() { michael@0: try { michael@0: if (!this.file) { michael@0: // it's not saved to file yet, it's in the webshell michael@0: michael@0: // get a temporary filename using the attributes from the data object that michael@0: // openInExternalEditor gave us michael@0: this.file = gViewSourceUtils.getTemporaryFile(this.data.uri, this.data.doc, michael@0: this.data.doc.contentType); michael@0: michael@0: // we have to convert from the source charset. michael@0: var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation); michael@0: var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"] michael@0: .createInstance(Components.interfaces.nsIFileOutputStream); michael@0: foStream.init(this.file, 0x02 | 0x08 | 0x20, -1, 0); // write | create | truncate michael@0: var coStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"] michael@0: .createInstance(Components.interfaces.nsIConverterOutputStream); michael@0: coStream.init(foStream, this.data.doc.characterSet, 0, null); michael@0: michael@0: // write the source to the file michael@0: coStream.writeString(webNavigation.document.body.textContent); michael@0: michael@0: // clean up michael@0: coStream.close(); michael@0: foStream.close(); michael@0: michael@0: let fromPrivateWindow = michael@0: this.data.doc.defaultView michael@0: .QueryInterface(Components.interfaces.nsIInterfaceRequestor) michael@0: .getInterface(Components.interfaces.nsIWebNavigation) michael@0: .QueryInterface(Components.interfaces.nsILoadContext) michael@0: .usePrivateBrowsing; michael@0: michael@0: let helperService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"] michael@0: .getService(Components.interfaces.nsPIExternalAppLauncher); michael@0: if (fromPrivateWindow) { michael@0: // register the file to be deleted when possible michael@0: helperService.deleteTemporaryPrivateFileWhenPossible(this.file); michael@0: } else { michael@0: // register the file to be deleted on app exit michael@0: helperService.deleteTemporaryFileOnExit(this.file); michael@0: } michael@0: } michael@0: michael@0: var editorArgs = gViewSourceUtils.buildEditorArgs(this.file.path, michael@0: this.data.lineNumber); michael@0: this.editor.runw(false, editorArgs, editorArgs.length); michael@0: michael@0: gViewSourceUtils.handleCallBack(this.callBack, true, this.data); michael@0: } catch (ex) { michael@0: // we failed loading it with the external editor. michael@0: Components.utils.reportError(ex); michael@0: gViewSourceUtils.handleCallBack(this.callBack, false, this.data); michael@0: } finally { michael@0: this.destroy(); michael@0: } michael@0: }, michael@0: michael@0: onLocationChange: function() {return 0;}, michael@0: onProgressChange: function() {return 0;}, michael@0: onStatusChange: function() {return 0;}, michael@0: onSecurityChange: function() {return 0;}, michael@0: michael@0: webShell: null, michael@0: editor: null, michael@0: callBack: null, michael@0: data: null, michael@0: file: null michael@0: }, michael@0: michael@0: // returns an nsIFile for the passed document in the system temp directory michael@0: getTemporaryFile: function(aURI, aDocument, aContentType) { michael@0: // include contentAreaUtils.js in our own context when we first need it michael@0: if (!this._caUtils) { michael@0: var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] michael@0: .getService(Components.interfaces.mozIJSSubScriptLoader); michael@0: this._caUtils = {}; michael@0: scriptLoader.loadSubScript("chrome://global/content/contentAreaUtils.js", this._caUtils); michael@0: } michael@0: michael@0: var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"] michael@0: .getService(Components.interfaces.nsIProperties); michael@0: var tempFile = fileLocator.get("TmpD", Components.interfaces.nsIFile); michael@0: var fileName = this._caUtils.getDefaultFileName(null, aURI, aDocument, aContentType); michael@0: var extension = this._caUtils.getDefaultExtension(fileName, aURI, aContentType); michael@0: var leafName = this._caUtils.getNormalizedLeafName(fileName, extension); michael@0: tempFile.append(leafName); michael@0: return tempFile; michael@0: } michael@0: }