toolkit/mozapps/downloads/nsHelperAppDlg.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */
     2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
     3 /*
     4 # This Source Code Form is subject to the terms of the Mozilla Public
     5 # License, v. 2.0. If a copy of the MPL was not distributed with this
     6 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
     7 */
     9 Components.utils.import("resource://gre/modules/Services.jsm");
    11 ///////////////////////////////////////////////////////////////////////////////
    12 //// Helper Functions
    14 /**
    15  * Determines if a given directory is able to be used to download to.
    16  *
    17  * @param aDirectory
    18  *        The directory to check.
    19  * @return true if we can use the directory, false otherwise.
    20  */
    21 function isUsableDirectory(aDirectory)
    22 {
    23   return aDirectory.exists() && aDirectory.isDirectory() &&
    24          aDirectory.isWritable();
    25 }
    27 // Web progress listener so we can detect errors while mLauncher is
    28 // streaming the data to a temporary file.
    29 function nsUnknownContentTypeDialogProgressListener(aHelperAppDialog) {
    30   this.helperAppDlg = aHelperAppDialog;
    31 }
    33 nsUnknownContentTypeDialogProgressListener.prototype = {
    34   // nsIWebProgressListener methods.
    35   // Look for error notifications and display alert to user.
    36   onStatusChange: function( aWebProgress, aRequest, aStatus, aMessage ) {
    37     if ( aStatus != Components.results.NS_OK ) {
    38       // Display error alert (using text supplied by back-end).
    39       // FIXME this.dialog is undefined?
    40       Services.prompt.alert( this.dialog, this.helperAppDlg.mTitle, aMessage );
    41       // Close the dialog.
    42       this.helperAppDlg.onCancel();
    43       if ( this.helperAppDlg.mDialog ) {
    44         this.helperAppDlg.mDialog.close();
    45       }
    46     }
    47   },
    49   // Ignore onProgressChange, onProgressChange64, onStateChange, onLocationChange, onSecurityChange, and onRefreshAttempted notifications.
    50   onProgressChange: function( aWebProgress,
    51                               aRequest,
    52                               aCurSelfProgress,
    53                               aMaxSelfProgress,
    54                               aCurTotalProgress,
    55                               aMaxTotalProgress ) {
    56   },
    58   onProgressChange64: function( aWebProgress,
    59                                 aRequest,
    60                                 aCurSelfProgress,
    61                                 aMaxSelfProgress,
    62                                 aCurTotalProgress,
    63                                 aMaxTotalProgress ) {
    64   },
    68   onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) {
    69   },
    71   onLocationChange: function( aWebProgress, aRequest, aLocation, aFlags ) {
    72   },
    74   onSecurityChange: function( aWebProgress, aRequest, state ) {
    75   },
    77   onRefreshAttempted: function( aWebProgress, aURI, aDelay, aSameURI ) {
    78     return true;
    79   }
    80 };
    82 ///////////////////////////////////////////////////////////////////////////////
    83 //// nsUnknownContentTypeDialog
    85 /* This file implements the nsIHelperAppLauncherDialog interface.
    86  *
    87  * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog,
    88  * comprised of:
    89  *   - a JS constructor function
    90  *   - a prototype providing all the interface methods and implementation stuff
    91  *
    92  * In addition, this file implements an nsIModule object that registers the
    93  * nsUnknownContentTypeDialog component.
    94  */
    96 const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
    97 const nsITimer = Components.interfaces.nsITimer;
    99 let downloadModule = {};
   100 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
   101 Components.utils.import("resource://gre/modules/DownloadLastDir.jsm", downloadModule);
   102 Components.utils.import("resource://gre/modules/DownloadPaths.jsm");
   103 Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
   104 Components.utils.import("resource://gre/modules/Downloads.jsm");
   105 Components.utils.import("resource://gre/modules/FileUtils.jsm");
   106 Components.utils.import("resource://gre/modules/Task.jsm");
   108 /* ctor
   109  */
   110 function nsUnknownContentTypeDialog() {
   111   // Initialize data properties.
   112   this.mLauncher = null;
   113   this.mContext  = null;
   114   this.chosenApp = null;
   115   this.givenDefaultApp = false;
   116   this.updateSelf = true;
   117   this.mTitle    = "";
   118 }
   120 nsUnknownContentTypeDialog.prototype = {
   121   classID: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"),
   123   nsIMIMEInfo  : Components.interfaces.nsIMIMEInfo,
   125   QueryInterface: function (iid) {
   126     if (!iid.equals(Components.interfaces.nsIHelperAppLauncherDialog) &&
   127         !iid.equals(Components.interfaces.nsITimerCallback) &&
   128         !iid.equals(Components.interfaces.nsISupports)) {
   129       throw Components.results.NS_ERROR_NO_INTERFACE;
   130     }
   131     return this;
   132   },
   134   // ---------- nsIHelperAppLauncherDialog methods ----------
   136   // show: Open XUL dialog using window watcher.  Since the dialog is not
   137   //       modal, it needs to be a top level window and the way to open
   138   //       one of those is via that route).
   139   show: function(aLauncher, aContext, aReason)  {
   140     this.mLauncher = aLauncher;
   141     this.mContext  = aContext;
   143     const nsITimer = Components.interfaces.nsITimer;
   144     this._showTimer = Components.classes["@mozilla.org/timer;1"]
   145                                 .createInstance(nsITimer);
   146     this._showTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT);
   147   },
   149   // When opening from new tab, if tab closes while dialog is opening,
   150   // (which is a race condition on the XUL file being cached and the timer
   151   // in nsExternalHelperAppService), the dialog gets a blur and doesn't
   152   // activate the OK button.  So we wait a bit before doing opening it.
   153   reallyShow: function() {
   154     try {
   155       var ir = this.mContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
   156       var dwi = ir.getInterface(Components.interfaces.nsIDOMWindow);
   157       var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
   158                          .getService(Components.interfaces.nsIWindowWatcher);
   159       this.mDialog = ww.openWindow(dwi,
   160                                    "chrome://mozapps/content/downloads/unknownContentType.xul",
   161                                    null,
   162                                    "chrome,centerscreen,titlebar,dialog=yes,dependent",
   163                                    null);
   164     } catch (ex) {
   165       // The containing window may have gone away.  Break reference
   166       // cycles and stop doing the download.
   167       this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED);
   168       return;
   169     }
   171     // Hook this object to the dialog.
   172     this.mDialog.dialog = this;
   174     // Hook up utility functions.
   175     this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey;
   177     // Watch for error notifications.
   178     var progressListener = new nsUnknownContentTypeDialogProgressListener(this);
   179     this.mLauncher.setWebProgressListener(progressListener);
   180   },
   182   //
   183   // displayBadPermissionAlert()
   184   //
   185   // Diplay an alert panel about the bad permission of folder/directory.
   186   //
   187   displayBadPermissionAlert: function () {
   188     let bundle =
   189       Services.strings.createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
   191     Services.prompt.alert(this.dialog,
   192                    bundle.GetStringFromName("badPermissions.title"),
   193                    bundle.GetStringFromName("badPermissions"));
   194   },
   196   // promptForSaveToFile:  Display file picker dialog and return selected file.
   197   //                       This is called by the External Helper App Service
   198   //                       after the ucth dialog calls |saveToDisk| with a null
   199   //                       target filename (no target, therefore user must pick).
   200   //
   201   //                       Alternatively, if the user has selected to have all
   202   //                       files download to a specific location, return that
   203   //                       location and don't ask via the dialog.
   204   //
   205   // Note - this function is called without a dialog, so it cannot access any part
   206   // of the dialog XUL as other functions on this object do.
   208   promptForSaveToFile: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
   209     throw new Components.Exception("Async version must be used", Components.results.NS_ERROR_NOT_AVAILABLE);
   210   },
   212   promptForSaveToFileAsync: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
   213     var result = null;
   215     this.mLauncher = aLauncher;
   217     let prefs = Components.classes["@mozilla.org/preferences-service;1"]
   218                           .getService(Components.interfaces.nsIPrefBranch);
   219     let bundle =
   220       Services.strings
   221               .createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
   223     Task.spawn(function() {
   224       if (!aForcePrompt) {
   225         // Check to see if the user wishes to auto save to the default download
   226         // folder without prompting. Note that preference might not be set.
   227         let autodownload = false;
   228         try {
   229           autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
   230         } catch (e) { }
   232         if (autodownload) {
   233           // Retrieve the user's default download directory
   234           let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
   235           let defaultFolder = new FileUtils.File(preferredDir);
   237           try {
   238             result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension);
   239           }
   240           catch (ex) {
   241             // When the default download directory is write-protected,
   242             // prompt the user for a different target file.
   243           }
   245           // Check to make sure we have a valid directory, otherwise, prompt
   246           if (result) {
   247             // This path is taken when we have a writable default download directory.
   248             aLauncher.saveDestinationAvailable(result);
   249             return;
   250           }
   251         }
   252       }
   254       // Use file picker to show dialog.
   255       var nsIFilePicker = Components.interfaces.nsIFilePicker;
   256       var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
   257       var windowTitle = bundle.GetStringFromName("saveDialogTitle");
   258       var parent = aContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);
   259       picker.init(parent, windowTitle, nsIFilePicker.modeSave);
   260       picker.defaultString = aDefaultFile;
   262       let gDownloadLastDir = new downloadModule.DownloadLastDir(parent);
   264       if (aSuggestedFileExtension) {
   265         // aSuggestedFileExtension includes the period, so strip it
   266         picker.defaultExtension = aSuggestedFileExtension.substring(1);
   267       }
   268       else {
   269         try {
   270           picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension;
   271         }
   272         catch (ex) { }
   273       }
   275       var wildCardExtension = "*";
   276       if (aSuggestedFileExtension) {
   277         wildCardExtension += aSuggestedFileExtension;
   278         picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension);
   279       }
   281       picker.appendFilters( nsIFilePicker.filterAll );
   283       // Default to lastDir if it is valid, otherwise use the user's default
   284       // downloads directory.  getPreferredDownloadsDirectory should always
   285       // return a valid directory path, so we can safely default to it.
   286       let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
   287       picker.displayDirectory = new FileUtils.File(preferredDir);
   289       gDownloadLastDir.getFileAsync(aLauncher.source, function LastDirCallback(lastDir) {
   290         if (lastDir && isUsableDirectory(lastDir))
   291           picker.displayDirectory = lastDir;
   293         if (picker.show() == nsIFilePicker.returnCancel) {
   294           // null result means user cancelled.
   295           aLauncher.saveDestinationAvailable(null);
   296           return;
   297         }
   299         // Be sure to save the directory the user chose through the Save As...
   300         // dialog  as the new browser.download.dir since the old one
   301         // didn't exist.
   302         result = picker.file;
   304         if (result) {
   305           try {
   306             // Remove the file so that it's not there when we ensure non-existence later;
   307             // this is safe because for the file to exist, the user would have had to
   308             // confirm that he wanted the file overwritten.
   309             if (result.exists())
   310               result.remove(false);
   311           }
   312           catch (ex) {
   313             // As it turns out, the failure to remove the file, for example due to
   314             // permission error, will be handled below eventually somehow.
   315           }
   317           var newDir = result.parent.QueryInterface(Components.interfaces.nsILocalFile);
   319           // Do not store the last save directory as a pref inside the private browsing mode
   320           gDownloadLastDir.setFile(aLauncher.source, newDir);
   322           try {
   323             result = this.validateLeafName(newDir, result.leafName, null);
   324           }
   325           catch (ex) {
   326             // When the chosen download directory is write-protected,
   327             // display an informative error message.
   328             // In all cases, download will be stopped.
   330             if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) {
   331               this.displayBadPermissionAlert();
   332               aLauncher.saveDestinationAvailable(null);
   333               return;
   334             }
   336           }
   337         }
   338         aLauncher.saveDestinationAvailable(result);
   339       }.bind(this));
   340     }.bind(this)).then(null, Components.utils.reportError);
   341   },
   343   /**
   344    * Ensures that a local folder/file combination does not already exist in
   345    * the file system (or finds such a combination with a reasonably similar
   346    * leaf name), creates the corresponding file, and returns it.
   347    *
   348    * @param   aLocalFolder
   349    *          the folder where the file resides
   350    * @param   aLeafName
   351    *          the string name of the file (may be empty if no name is known,
   352    *          in which case a name will be chosen)
   353    * @param   aFileExt
   354    *          the extension of the file, if one is known; this will be ignored
   355    *          if aLeafName is non-empty
   356    * @return  nsILocalFile
   357    *          the created file
   358    * @throw   an error such as permission doesn't allow creation of
   359    *          file, etc.
   360    */
   361   validateLeafName: function (aLocalFolder, aLeafName, aFileExt)
   362   {
   363     if (!(aLocalFolder && isUsableDirectory(aLocalFolder))) {
   364       throw new Components.Exception("Destination directory non-existing or permission error",
   365                                      Components.results.NS_ERROR_FILE_ACCESS_DENIED);
   366     }
   367     // Remove any leading periods, since we don't want to save hidden files
   368     // automatically.
   369     aLeafName = aLeafName.replace(/^\.+/, "");
   371     if (aLeafName == "")
   372       aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
   373     aLocalFolder.append(aLeafName);
   375     // The following assignment can throw an exception, but
   376     // is now caught properly in the caller of validateLeafName.
   377     var createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder);
   379 #ifdef XP_WIN
   380     let ext;
   381     try {
   382       // We can fail here if there's no primary extension set
   383       ext = "." + this.mLauncher.MIMEInfo.primaryExtension;
   384     } catch (e) { }
   386     // Append a file extension if it's an executable that doesn't have one
   387     // but make sure we actually have an extension to add
   388     let leaf = createdFile.leafName;
   389     if (ext && leaf.slice(-ext.length) != ext && createdFile.isExecutable()) {
   390       createdFile.remove(false);
   391       aLocalFolder.leafName = leaf + ext;
   392       createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder);
   393     }
   394 #endif
   396     return createdFile;
   397   },
   399   // ---------- implementation methods ----------
   401   // initDialog:  Fill various dialog fields with initial content.
   402   initDialog : function() {
   403     // Put file name in window title.
   404     var suggestedFileName = this.mLauncher.suggestedFileName;
   406     // Some URIs do not implement nsIURL, so we can't just QI.
   407     var url = this.mLauncher.source;
   408     if (url instanceof Components.interfaces.nsINestedURI)
   409       url = url.innermostURI;
   411     var fname = "";
   412     var iconPath = "goat";
   413     this.mSourcePath = url.prePath;
   414     if (url instanceof Components.interfaces.nsIURL) {
   415       // A url, use file name from it.
   416       fname = iconPath = url.fileName;
   417       this.mSourcePath += url.directory;
   418     } else {
   419       // A generic uri, use path.
   420       fname = url.path;
   421       this.mSourcePath += url.path;
   422     }
   424     if (suggestedFileName)
   425       fname = iconPath = suggestedFileName;
   427     var displayName = fname.replace(/ +/g, " ");
   429     this.mTitle = this.dialogElement("strings").getFormattedString("title", [displayName]);
   430     this.mDialog.document.title = this.mTitle;
   432     // Put content type, filename and location into intro.
   433     this.initIntro(url, fname, displayName);
   435     var iconString = "moz-icon://" + iconPath + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType;
   436     this.dialogElement("contentTypeImage").setAttribute("src", iconString);
   438     // if always-save and is-executable and no-handler
   439     // then set up simple ui
   440     var mimeType = this.mLauncher.MIMEInfo.MIMEType;
   441     var shouldntRememberChoice = (mimeType == "application/octet-stream" ||
   442                                   mimeType == "application/x-msdownload" ||
   443                                   this.mLauncher.targetFileIsExecutable);
   444     if (shouldntRememberChoice && !this.openWithDefaultOK()) {
   445       // hide featured choice
   446       this.dialogElement("normalBox").collapsed = true;
   447       // show basic choice
   448       this.dialogElement("basicBox").collapsed = false;
   449       // change button labels and icons; use "save" icon for the accept
   450       // button since it's the only action possible
   451       let acceptButton = this.mDialog.document.documentElement
   452                                               .getButton("accept");
   453       acceptButton.label = this.dialogElement("strings")
   454                                .getString("unknownAccept.label");
   455       acceptButton.setAttribute("icon", "save");
   456       this.mDialog.document.documentElement.getButton("cancel").label = this.dialogElement("strings").getString("unknownCancel.label");
   457       // hide other handler
   458       this.dialogElement("openHandler").collapsed = true;
   459       // set save as the selected option
   460       this.dialogElement("mode").selectedItem = this.dialogElement("save");
   461     }
   462     else {
   463       this.initAppAndSaveToDiskValues();
   465       // Initialize "always ask me" box. This should always be disabled
   466       // and set to true for the ambiguous type application/octet-stream.
   467       // We don't also check for application/x-msdownload here since we
   468       // want users to be able to autodownload .exe files.
   469       var rememberChoice = this.dialogElement("rememberChoice");
   471 #if 0
   472       // Just because we have a content-type of application/octet-stream
   473       // here doesn't actually mean that the content is of that type. Many
   474       // servers default to sending text/plain for file types they don't know
   475       // about. To account for this, the uriloader does some checking to see
   476       // if a file sent as text/plain contains binary characters, and if so (*)
   477       // it morphs the content-type into application/octet-stream so that
   478       // the file can be properly handled. Since this is not generic binary
   479       // data, rather, a data format that the system probably knows about,
   480       // we don't want to use the content-type provided by this dialog's
   481       // opener, as that's the generic application/octet-stream that the
   482       // uriloader has passed, rather we want to ask the MIME Service.
   483       // This is so we don't needlessly disable the "autohandle" checkbox.
   485       // commented out to close the opening brace in the if statement.
   486       // var mimeService = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService);
   487       // var type = mimeService.getTypeFromURI(this.mLauncher.source);
   488       // this.realMIMEInfo = mimeService.getFromTypeAndExtension(type, "");
   490       // if (type == "application/octet-stream") {
   491 #endif
   492       if (shouldntRememberChoice) {
   493         rememberChoice.checked = false;
   494         rememberChoice.disabled = true;
   495       }
   496       else {
   497         rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling &&
   498                                  this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.handleInternally;
   499       }
   500       this.toggleRememberChoice(rememberChoice);
   502       // XXXben - menulist won't init properly, hack.
   503       var openHandler = this.dialogElement("openHandler");
   504       openHandler.parentNode.removeChild(openHandler);
   505       var openHandlerBox = this.dialogElement("openHandlerBox");
   506       openHandlerBox.appendChild(openHandler);
   507     }
   509     this.mDialog.setTimeout("dialog.postShowCallback()", 0);
   511     let acceptDelay = Services.prefs.getIntPref("security.dialog_enable_delay");
   512     this.mDialog.document.documentElement.getButton("accept").disabled = true;
   513     this._showTimer = Components.classes["@mozilla.org/timer;1"]
   514                                 .createInstance(nsITimer);
   515     this._showTimer.initWithCallback(this, acceptDelay, nsITimer.TYPE_ONE_SHOT);
   516   },
   518   notify: function (aTimer) {
   519     if (aTimer == this._showTimer) {
   520       if (!this.mDialog) {
   521         this.reallyShow();
   522       } else {
   523         // The user may have already canceled the dialog.
   524         try {
   525           if (!this._blurred) {
   526             this.mDialog.document.documentElement.getButton("accept").disabled = false;
   527           }
   528         } catch (ex) {}
   529         this._delayExpired = true;
   530       }
   531       // The timer won't release us, so we have to release it.
   532       this._showTimer = null;
   533     }
   534     else if (aTimer == this._saveToDiskTimer) {
   535       // Since saveToDisk may open a file picker and therefore block this routine,
   536       // we should only call it once the dialog is closed.
   537       this.mLauncher.saveToDisk(null, false);
   538       this._saveToDiskTimer = null;
   539     }
   540   },
   542   postShowCallback: function () {
   543     this.mDialog.sizeToContent();
   545     // Set initial focus
   546     this.dialogElement("mode").focus();
   547   },
   549   // initIntro:
   550   initIntro: function(url, filename, displayname) {
   551     this.dialogElement( "location" ).value = displayname;
   552     this.dialogElement( "location" ).setAttribute("realname", filename);
   553     this.dialogElement( "location" ).setAttribute("tooltiptext", displayname);
   555     // if mSourcePath is a local file, then let's use the pretty path name
   556     // instead of an ugly url...
   557     var pathString;
   558     if (url instanceof Components.interfaces.nsIFileURL) {
   559       try {
   560         // Getting .file might throw, or .parent could be null
   561         pathString = url.file.parent.path;
   562       } catch (ex) {}
   563     }
   565     if (!pathString) {
   566       // wasn't a fileURL
   567       var tmpurl = url.clone(); // don't want to change the real url
   568       try {
   569         tmpurl.userPass = "";
   570       } catch (ex) {}
   571       pathString = tmpurl.prePath;
   572     }
   574     // Set the location text, which is separate from the intro text so it can be cropped
   575     var location = this.dialogElement( "source" );
   576     location.value = pathString;
   577     location.setAttribute("tooltiptext", this.mSourcePath);
   579     // Show the type of file.
   580     var type = this.dialogElement("type");
   581     var mimeInfo = this.mLauncher.MIMEInfo;
   583     // 1. Try to use the pretty description of the type, if one is available.
   584     var typeString = mimeInfo.description;
   586     if (typeString == "") {
   587       // 2. If there is none, use the extension to identify the file, e.g. "ZIP file"
   588       var primaryExtension = "";
   589       try {
   590         primaryExtension = mimeInfo.primaryExtension;
   591       }
   592       catch (ex) {
   593       }
   594       if (primaryExtension != "")
   595         typeString = this.dialogElement("strings").getFormattedString("fileType", [primaryExtension.toUpperCase()]);
   596       // 3. If we can't even do that, just give up and show the MIME type.
   597       else
   598         typeString = mimeInfo.MIMEType;
   599     }
   600     // When the length is unknown, contentLength would be -1
   601     if (this.mLauncher.contentLength >= 0) {
   602       let [size, unit] = DownloadUtils.
   603                          convertByteUnits(this.mLauncher.contentLength);
   604       type.value = this.dialogElement("strings")
   605                        .getFormattedString("orderedFileSizeWithType", 
   606                                            [typeString, size, unit]);
   607     }
   608     else {
   609       type.value = typeString;
   610     }
   611   },
   613   _blurred: false,
   614   _delayExpired: false,
   615   onBlur: function(aEvent) {
   616     this._blurred = true;
   617     this.mDialog.document.documentElement.getButton("accept").disabled = true;
   618   },
   620   onFocus: function(aEvent) {
   621     this._blurred = false;
   622     if (this._delayExpired) {
   623       var script = "document.documentElement.getButton('accept').disabled = false";
   624       this.mDialog.setTimeout(script, 250);
   625     }
   626   },
   628   // Returns true if opening the default application makes sense.
   629   openWithDefaultOK: function() {
   630     // The checking is different on Windows...
   631 #ifdef XP_WIN
   632     // Windows presents some special cases.
   633     // We need to prevent use of "system default" when the file is
   634     // executable (so the user doesn't launch nasty programs downloaded
   635     // from the web), and, enable use of "system default" if it isn't
   636     // executable (because we will prompt the user for the default app
   637     // in that case).
   639     //  Default is Ok if the file isn't executable (and vice-versa).
   640     return !this.mLauncher.targetFileIsExecutable;
   641 #else
   642     // On other platforms, default is Ok if there is a default app.
   643     // Note that nsIMIMEInfo providers need to ensure that this holds true
   644     // on each platform.
   645     return this.mLauncher.MIMEInfo.hasDefaultHandler;
   646 #endif
   647   },
   649   // Set "default" application description field.
   650   initDefaultApp: function() {
   651     // Use description, if we can get one.
   652     var desc = this.mLauncher.MIMEInfo.defaultDescription;
   653     if (desc) {
   654       var defaultApp = this.dialogElement("strings").getFormattedString("defaultApp", [desc]);
   655       this.dialogElement("defaultHandler").label = defaultApp;
   656     }
   657     else {
   658       this.dialogElement("modeDeck").setAttribute("selectedIndex", "1");
   659       // Hide the default handler item too, in case the user picks a
   660       // custom handler at a later date which triggers the menulist to show.
   661       this.dialogElement("defaultHandler").hidden = true;
   662     }
   663   },
   665   // getPath:
   666   getPath: function (aFile) {
   667 #ifdef XP_MACOSX
   668     return aFile.leafName || aFile.path;
   669 #else
   670     return aFile.path;
   671 #endif
   672   },
   674   // initAppAndSaveToDiskValues:
   675   initAppAndSaveToDiskValues: function() {
   676     var modeGroup = this.dialogElement("mode");
   678     // We don't let users open .exe files or random binary data directly
   679     // from the browser at the moment because of security concerns.
   680     var openWithDefaultOK = this.openWithDefaultOK();
   681     var mimeType = this.mLauncher.MIMEInfo.MIMEType;
   682     if (this.mLauncher.targetFileIsExecutable || (
   683       (mimeType == "application/octet-stream" ||
   684        mimeType == "application/x-msdownload") &&
   685         !openWithDefaultOK)) {
   686       this.dialogElement("open").disabled = true;
   687       var openHandler = this.dialogElement("openHandler");
   688       openHandler.disabled = true;
   689       openHandler.selectedItem = null;
   690       modeGroup.selectedItem = this.dialogElement("save");
   691       return;
   692     }
   694     // Fill in helper app info, if there is any.
   695     try {
   696       this.chosenApp =
   697         this.mLauncher.MIMEInfo.preferredApplicationHandler
   698                                .QueryInterface(Components.interfaces.nsILocalHandlerApp);
   699     } catch (e) {
   700       this.chosenApp = null;
   701     }
   702     // Initialize "default application" field.
   703     this.initDefaultApp();
   705     var otherHandler = this.dialogElement("otherHandler");
   707     // Fill application name textbox.
   708     if (this.chosenApp && this.chosenApp.executable &&
   709         this.chosenApp.executable.path) {
   710       otherHandler.setAttribute("path",
   711                                 this.getPath(this.chosenApp.executable));
   713       otherHandler.label = this.getFileDisplayName(this.chosenApp.executable);
   714       otherHandler.hidden = false;
   715     }
   717     var useDefault = this.dialogElement("useSystemDefault");
   718     var openHandler = this.dialogElement("openHandler");
   719     openHandler.selectedIndex = 0;
   721     if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useSystemDefault) {
   722       // Open (using system default).
   723       modeGroup.selectedItem = this.dialogElement("open");
   724     } else if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp) {
   725       // Open with given helper app.
   726       modeGroup.selectedItem = this.dialogElement("open");
   727       openHandler.selectedIndex = 1;
   728     } else {
   729       // Save to disk.
   730       modeGroup.selectedItem = this.dialogElement("save");
   731     }
   733     // If we don't have a "default app" then disable that choice.
   734     if (!openWithDefaultOK) {
   735       var useDefault = this.dialogElement("defaultHandler");
   736       var isSelected = useDefault.selected;
   738       // Disable that choice.
   739       useDefault.hidden = true;
   740       // If that's the default, then switch to "save to disk."
   741       if (isSelected) {
   742         openHandler.selectedIndex = 1;
   743         modeGroup.selectedItem = this.dialogElement("save");
   744       }
   745     }
   747     otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false;
   748     this.updateOKButton();
   749   },
   751   // Returns the user-selected application
   752   helperAppChoice: function() {
   753     return this.chosenApp;
   754   },
   756   get saveToDisk() {
   757     return this.dialogElement("save").selected;
   758   },
   760   get useOtherHandler() {
   761     return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1;
   762   },
   764   get useSystemDefault() {
   765     return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0;
   766   },
   768   toggleRememberChoice: function (aCheckbox) {
   769     this.dialogElement("settingsChange").hidden = !aCheckbox.checked;
   770     this.mDialog.sizeToContent();
   771   },
   773   openHandlerCommand: function () {
   774     var openHandler = this.dialogElement("openHandler");
   775     if (openHandler.selectedItem.id == "choose")
   776       this.chooseApp();
   777     else
   778       openHandler.setAttribute("lastSelectedItemID", openHandler.selectedItem.id);
   779   },
   781   updateOKButton: function() {
   782     var ok = false;
   783     if (this.dialogElement("save").selected) {
   784       // This is always OK.
   785       ok = true;
   786     }
   787     else if (this.dialogElement("open").selected) {
   788       switch (this.dialogElement("openHandler").selectedIndex) {
   789       case 0:
   790         // No app need be specified in this case.
   791         ok = true;
   792         break;
   793       case 1:
   794         // only enable the OK button if we have a default app to use or if
   795         // the user chose an app....
   796         ok = this.chosenApp || /\S/.test(this.dialogElement("otherHandler").getAttribute("path")); 
   797         break;
   798       }
   799     }
   801     // Enable Ok button if ok to press.
   802     this.mDialog.document.documentElement.getButton("accept").disabled = !ok;
   803   },
   805   // Returns true iff the user-specified helper app has been modified.
   806   appChanged: function() {
   807     return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler;
   808   },
   810   updateMIMEInfo: function() {
   811     // Don't update mime type preferences when the preferred action is set to
   812     // the internal handler -- this dialog is the result of the handler fallback
   813     // (e.g. Content-Disposition was set as attachment)
   814     var discardUpdate = this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.handleInternally &&
   815                         !this.dialogElement("rememberChoice").checked;
   817     var needUpdate = false;
   818     // If current selection differs from what's in the mime info object,
   819     // then we need to update.
   820     if (this.saveToDisk) {
   821       needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk;
   822       if (needUpdate)
   823         this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk;
   824     }
   825     else if (this.useSystemDefault) {
   826       needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useSystemDefault;
   827       if (needUpdate)
   828         this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useSystemDefault;
   829     }
   830     else {
   831       // For "open with", we need to check both preferred action and whether the user chose
   832       // a new app.
   833       needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useHelperApp || this.appChanged();
   834       if (needUpdate) {
   835         this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp;
   836         // App may have changed - Update application
   837         var app = this.helperAppChoice();
   838         this.mLauncher.MIMEInfo.preferredApplicationHandler = app;
   839       }
   840     }
   841     // We will also need to update if the "always ask" flag has changed.
   842     needUpdate = needUpdate || this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != (!this.dialogElement("rememberChoice").checked);
   844     // One last special case: If the input "always ask" flag was false, then we always
   845     // update.  In that case we are displaying the helper app dialog for the first
   846     // time for this mime type and we need to store the user's action in the mimeTypes.rdf
   847     // data source (whether that action has changed or not; if it didn't change, then we need
   848     // to store the "always ask" flag so the helper app dialog will or won't display
   849     // next time, per the user's selection).
   850     needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling;
   852     // Make sure mime info has updated setting for the "always ask" flag.
   853     this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked;
   855     return needUpdate && !discardUpdate;
   856   },
   858   // See if the user changed things, and if so, update the
   859   // mimeTypes.rdf entry for this mime type.
   860   updateHelperAppPref: function() {
   861     var ha = new this.mDialog.HelperApps();
   862     ha.updateTypeInfo(this.mLauncher.MIMEInfo);
   863     ha.destroy();
   864   },
   866   // onOK:
   867   onOK: function() {
   868     // Verify typed app path, if necessary.
   869     if (this.useOtherHandler) {
   870       var helperApp = this.helperAppChoice();
   871       if (!helperApp || !helperApp.executable ||
   872           !helperApp.executable.exists()) {
   873         // Show alert and try again.
   874         var bundle = this.dialogElement("strings");
   875         var msg = bundle.getFormattedString("badApp", [this.dialogElement("otherHandler").getAttribute("path")]);
   876         Services.prompt.alert(this.mDialog, bundle.getString("badApp.title"), msg);
   878         // Disable the OK button.
   879         this.mDialog.document.documentElement.getButton("accept").disabled = true;
   880         this.dialogElement("mode").focus();
   882         // Clear chosen application.
   883         this.chosenApp = null;
   885         // Leave dialog up.
   886         return false;
   887       }
   888     }
   890     // Remove our web progress listener (a progress dialog will be
   891     // taking over).
   892     this.mLauncher.setWebProgressListener(null);
   894     // saveToDisk and launchWithApplication can return errors in
   895     // certain circumstances (e.g. The user clicks cancel in the
   896     // "Save to Disk" dialog. In those cases, we don't want to
   897     // update the helper application preferences in the RDF file.
   898     try {
   899       var needUpdate = this.updateMIMEInfo();
   901       if (this.dialogElement("save").selected) {
   902         // If we're using a default download location, create a path
   903         // for the file to be saved to to pass to |saveToDisk| - otherwise
   904         // we must ask the user to pick a save name.
   906 #if 0
   907         var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
   908         var targetFile = null;
   909         try {
   910           targetFile = prefs.getComplexValue("browser.download.defaultFolder",
   911                                              Components.interfaces.nsILocalFile);
   912           var leafName = this.dialogElement("location").getAttribute("realname");
   913           // Ensure that we don't overwrite any existing files here.
   914           targetFile = this.validateLeafName(targetFile, leafName, null);
   915         }
   916         catch(e) { }
   918         this.mLauncher.saveToDisk(targetFile, false);
   919 #endif
   921         // see @notify
   922         // we cannot use opener's setTimeout, see bug 420405
   923         this._saveToDiskTimer = Components.classes["@mozilla.org/timer;1"]
   924                                           .createInstance(nsITimer);
   925         this._saveToDiskTimer.initWithCallback(this, 0,
   926                                                nsITimer.TYPE_ONE_SHOT);
   927       }
   928       else
   929         this.mLauncher.launchWithApplication(null, false);
   931       // Update user pref for this mime type (if necessary). We do not
   932       // store anything in the mime type preferences for the ambiguous
   933       // type application/octet-stream. We do NOT do this for
   934       // application/x-msdownload since we want users to be able to
   935       // autodownload these to disk.
   936       if (needUpdate && this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream")
   937         this.updateHelperAppPref();
   938     } catch(e) { }
   940     // Unhook dialog from this object.
   941     this.mDialog.dialog = null;
   943     // Close up dialog by returning true.
   944     return true;
   945   },
   947   // onCancel:
   948   onCancel: function() {
   949     // Remove our web progress listener.
   950     this.mLauncher.setWebProgressListener(null);
   952     // Cancel app launcher.
   953     try {
   954       this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED);
   955     } catch(exception) {
   956     }
   958     // Unhook dialog from this object.
   959     this.mDialog.dialog = null;
   961     // Close up dialog by returning true.
   962     return true;
   963   },
   965   // dialogElement:  Convenience.
   966   dialogElement: function(id) {
   967     return this.mDialog.document.getElementById(id);
   968   },
   970   // Retrieve the pretty description from the file
   971   getFileDisplayName: function getFileDisplayName(file)
   972   {
   973 #ifdef XP_WIN
   974     if (file instanceof Components.interfaces.nsILocalFileWin) {
   975       try {
   976         return file.getVersionInfoField("FileDescription");
   977       } catch (e) {}
   978     }
   979 #endif
   980 #ifdef XP_MACOSX
   981     if (file instanceof Components.interfaces.nsILocalFileMac) {
   982       try {
   983         return file.bundleDisplayName;
   984       } catch (e) {}
   985     }
   986 #endif
   987     return file.leafName;
   988   },
   990   // chooseApp:  Open file picker and prompt user for application.
   991   chooseApp: function() {
   992 #ifdef XP_WIN
   993     // Protect against the lack of an extension
   994     var fileExtension = "";
   995     try {
   996       fileExtension = this.mLauncher.MIMEInfo.primaryExtension;
   997     } catch(ex) {
   998     }
  1000     // Try to use the pretty description of the type, if one is available.
  1001     var typeString = this.mLauncher.MIMEInfo.description;
  1003     if (!typeString) {
  1004       // If there is none, use the extension to
  1005       // identify the file, e.g. "ZIP file"
  1006       if (fileExtension) {
  1007         typeString =
  1008           this.dialogElement("strings").
  1009           getFormattedString("fileType", [fileExtension.toUpperCase()]);
  1010       } else {
  1011         // If we can't even do that, just give up and show the MIME type.
  1012         typeString = this.mLauncher.MIMEInfo.MIMEType;
  1016     var params = {};
  1017     params.title =
  1018       this.dialogElement("strings").getString("chooseAppFilePickerTitle");
  1019     params.description = typeString;
  1020     params.filename    = this.mLauncher.suggestedFileName;
  1021     params.mimeInfo    = this.mLauncher.MIMEInfo;
  1022     params.handlerApp  = null;
  1024     this.mDialog.openDialog("chrome://global/content/appPicker.xul", null,
  1025                             "chrome,modal,centerscreen,titlebar,dialog=yes",
  1026                             params);
  1028     if (params.handlerApp &&
  1029         params.handlerApp.executable &&
  1030         params.handlerApp.executable.isFile()) {
  1031       // Remember the file they chose to run.
  1032       this.chosenApp = params.handlerApp;
  1034 #else
  1035     var nsIFilePicker = Components.interfaces.nsIFilePicker;
  1036     var fp = Components.classes["@mozilla.org/filepicker;1"]
  1037                        .createInstance(nsIFilePicker);
  1038     fp.init(this.mDialog,
  1039             this.dialogElement("strings").getString("chooseAppFilePickerTitle"),
  1040             nsIFilePicker.modeOpen);
  1042     fp.appendFilters(nsIFilePicker.filterApps);
  1044     if (fp.show() == nsIFilePicker.returnOK && fp.file) {
  1045       // Remember the file they chose to run.
  1046       var localHandlerApp =
  1047         Components.classes["@mozilla.org/uriloader/local-handler-app;1"].
  1048                    createInstance(Components.interfaces.nsILocalHandlerApp);
  1049       localHandlerApp.executable = fp.file;
  1050       this.chosenApp = localHandlerApp;
  1051 #endif
  1053       // Show the "handler" menulist since we have a (user-specified)
  1054       // application now.
  1055       this.dialogElement("modeDeck").setAttribute("selectedIndex", "0");
  1057       // Update dialog.
  1058       var otherHandler = this.dialogElement("otherHandler");
  1059       otherHandler.removeAttribute("hidden");
  1060       otherHandler.setAttribute("path", this.getPath(this.chosenApp.executable));
  1061       otherHandler.label = this.getFileDisplayName(this.chosenApp.executable);
  1062       this.dialogElement("openHandler").selectedIndex = 1;
  1063       this.dialogElement("openHandler").setAttribute("lastSelectedItemID", "otherHandler");
  1065       this.dialogElement("mode").selectedItem = this.dialogElement("open");
  1067     else {
  1068       var openHandler = this.dialogElement("openHandler");
  1069       var lastSelectedID = openHandler.getAttribute("lastSelectedItemID");
  1070       if (!lastSelectedID)
  1071         lastSelectedID = "defaultHandler";
  1072       openHandler.selectedItem = this.dialogElement(lastSelectedID);
  1074   },
  1076   // Turn this on to get debugging messages.
  1077   debug: false,
  1079   // Dump text (if debug is on).
  1080   dump: function( text ) {
  1081     if ( this.debug ) {
  1082       dump( text );
  1084   },
  1086   // dumpObj:
  1087   dumpObj: function( spec ) {
  1088     var val = "<undefined>";
  1089     try {
  1090       val = eval( "this."+spec ).toString();
  1091     } catch( exception ) {
  1093     this.dump( spec + "=" + val + "\n" );
  1094   },
  1096   // dumpObjectProperties
  1097   dumpObjectProperties: function( desc, obj ) {
  1098     for( prop in obj ) {
  1099       this.dump( desc + "." + prop + "=" );
  1100       var val = "<undefined>";
  1101       try {
  1102         val = obj[ prop ];
  1103       } catch ( exception ) {
  1105       this.dump( val + "\n" );
  1110 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsUnknownContentTypeDialog]);

mercurial