toolkit/components/viewsource/content/viewSourceUtils.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 // -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
     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  */
    15 var gViewSourceUtils = {
    17   mnsIWebBrowserPersist: Components.interfaces.nsIWebBrowserPersist,
    18   mnsIWebProgress: Components.interfaces.nsIWebProgress,
    19   mnsIWebPageDescriptor: Components.interfaces.nsIWebPageDescriptor,
    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   },
    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   },
    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   },
    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};
    81     try {
    82       var editor = this.getExternalViewSourceEditor();    
    83       if (!editor) {
    84         this.handleCallBack(aCallBack, false, data);
    85         return;
    86       }
    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;
    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;
   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;
   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           }
   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);
   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   },
   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   },
   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   },
   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);
   200       return editor;
   201     }
   202     catch (ex) {
   203       Components.utils.reportError(ex);
   204     }
   206     return null;
   207   },
   209   viewSourceProgressListener: {
   211     mnsIWebProgressListener: Components.interfaces.nsIWebProgressListener,
   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     },
   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     },
   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     },
   256     onContentLoaded: function() {
   257       try {
   258         if (!this.file) {
   259           // it's not saved to file yet, it's in the webshell
   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);
   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);
   275           // write the source to the file
   276           coStream.writeString(webNavigation.document.body.textContent);
   278           // clean up
   279           coStream.close();
   280           foStream.close();
   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;
   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         }
   300         var editorArgs = gViewSourceUtils.buildEditorArgs(this.file.path,
   301                                                           this.data.lineNumber);
   302         this.editor.runw(false, editorArgs, editorArgs.length);
   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     },
   314     onLocationChange: function() {return 0;},
   315     onProgressChange: function() {return 0;},
   316     onStatusChange: function() {return 0;},
   317     onSecurityChange: function() {return 0;},
   319     webShell: null,
   320     editor: null,
   321     callBack: null,
   322     data: null,
   323     file: null
   324   },
   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     }
   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 }

mercurial