toolkit/mozapps/downloads/nsHelperAppDlg.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:f9bfc82f4ca7
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 */
8
9 Components.utils.import("resource://gre/modules/Services.jsm");
10
11 ///////////////////////////////////////////////////////////////////////////////
12 //// Helper Functions
13
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 }
26
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 }
32
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 },
48
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 },
57
58 onProgressChange64: function( aWebProgress,
59 aRequest,
60 aCurSelfProgress,
61 aMaxSelfProgress,
62 aCurTotalProgress,
63 aMaxTotalProgress ) {
64 },
65
66
67
68 onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) {
69 },
70
71 onLocationChange: function( aWebProgress, aRequest, aLocation, aFlags ) {
72 },
73
74 onSecurityChange: function( aWebProgress, aRequest, state ) {
75 },
76
77 onRefreshAttempted: function( aWebProgress, aURI, aDelay, aSameURI ) {
78 return true;
79 }
80 };
81
82 ///////////////////////////////////////////////////////////////////////////////
83 //// nsUnknownContentTypeDialog
84
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 */
95
96 const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
97 const nsITimer = Components.interfaces.nsITimer;
98
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");
107
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 }
119
120 nsUnknownContentTypeDialog.prototype = {
121 classID: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"),
122
123 nsIMIMEInfo : Components.interfaces.nsIMIMEInfo,
124
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 },
133
134 // ---------- nsIHelperAppLauncherDialog methods ----------
135
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;
142
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 },
148
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 }
170
171 // Hook this object to the dialog.
172 this.mDialog.dialog = this;
173
174 // Hook up utility functions.
175 this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey;
176
177 // Watch for error notifications.
178 var progressListener = new nsUnknownContentTypeDialogProgressListener(this);
179 this.mLauncher.setWebProgressListener(progressListener);
180 },
181
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");
190
191 Services.prompt.alert(this.dialog,
192 bundle.GetStringFromName("badPermissions.title"),
193 bundle.GetStringFromName("badPermissions"));
194 },
195
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.
207
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 },
211
212 promptForSaveToFileAsync: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
213 var result = null;
214
215 this.mLauncher = aLauncher;
216
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");
222
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) { }
231
232 if (autodownload) {
233 // Retrieve the user's default download directory
234 let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
235 let defaultFolder = new FileUtils.File(preferredDir);
236
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 }
244
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 }
253
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;
261
262 let gDownloadLastDir = new downloadModule.DownloadLastDir(parent);
263
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 }
274
275 var wildCardExtension = "*";
276 if (aSuggestedFileExtension) {
277 wildCardExtension += aSuggestedFileExtension;
278 picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension);
279 }
280
281 picker.appendFilters( nsIFilePicker.filterAll );
282
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);
288
289 gDownloadLastDir.getFileAsync(aLauncher.source, function LastDirCallback(lastDir) {
290 if (lastDir && isUsableDirectory(lastDir))
291 picker.displayDirectory = lastDir;
292
293 if (picker.show() == nsIFilePicker.returnCancel) {
294 // null result means user cancelled.
295 aLauncher.saveDestinationAvailable(null);
296 return;
297 }
298
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;
303
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 }
316
317 var newDir = result.parent.QueryInterface(Components.interfaces.nsILocalFile);
318
319 // Do not store the last save directory as a pref inside the private browsing mode
320 gDownloadLastDir.setFile(aLauncher.source, newDir);
321
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.
329
330 if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) {
331 this.displayBadPermissionAlert();
332 aLauncher.saveDestinationAvailable(null);
333 return;
334 }
335
336 }
337 }
338 aLauncher.saveDestinationAvailable(result);
339 }.bind(this));
340 }.bind(this)).then(null, Components.utils.reportError);
341 },
342
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(/^\.+/, "");
370
371 if (aLeafName == "")
372 aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
373 aLocalFolder.append(aLeafName);
374
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);
378
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) { }
385
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
395
396 return createdFile;
397 },
398
399 // ---------- implementation methods ----------
400
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;
405
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;
410
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 }
423
424 if (suggestedFileName)
425 fname = iconPath = suggestedFileName;
426
427 var displayName = fname.replace(/ +/g, " ");
428
429 this.mTitle = this.dialogElement("strings").getFormattedString("title", [displayName]);
430 this.mDialog.document.title = this.mTitle;
431
432 // Put content type, filename and location into intro.
433 this.initIntro(url, fname, displayName);
434
435 var iconString = "moz-icon://" + iconPath + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType;
436 this.dialogElement("contentTypeImage").setAttribute("src", iconString);
437
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();
464
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");
470
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.
484
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, "");
489
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);
501
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 }
508
509 this.mDialog.setTimeout("dialog.postShowCallback()", 0);
510
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 },
517
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 },
541
542 postShowCallback: function () {
543 this.mDialog.sizeToContent();
544
545 // Set initial focus
546 this.dialogElement("mode").focus();
547 },
548
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);
554
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 }
564
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 }
573
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);
578
579 // Show the type of file.
580 var type = this.dialogElement("type");
581 var mimeInfo = this.mLauncher.MIMEInfo;
582
583 // 1. Try to use the pretty description of the type, if one is available.
584 var typeString = mimeInfo.description;
585
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 },
612
613 _blurred: false,
614 _delayExpired: false,
615 onBlur: function(aEvent) {
616 this._blurred = true;
617 this.mDialog.document.documentElement.getButton("accept").disabled = true;
618 },
619
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 },
627
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).
638
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 },
648
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 },
664
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 },
673
674 // initAppAndSaveToDiskValues:
675 initAppAndSaveToDiskValues: function() {
676 var modeGroup = this.dialogElement("mode");
677
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 }
693
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();
704
705 var otherHandler = this.dialogElement("otherHandler");
706
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));
712
713 otherHandler.label = this.getFileDisplayName(this.chosenApp.executable);
714 otherHandler.hidden = false;
715 }
716
717 var useDefault = this.dialogElement("useSystemDefault");
718 var openHandler = this.dialogElement("openHandler");
719 openHandler.selectedIndex = 0;
720
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 }
732
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;
737
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 }
746
747 otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false;
748 this.updateOKButton();
749 },
750
751 // Returns the user-selected application
752 helperAppChoice: function() {
753 return this.chosenApp;
754 },
755
756 get saveToDisk() {
757 return this.dialogElement("save").selected;
758 },
759
760 get useOtherHandler() {
761 return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1;
762 },
763
764 get useSystemDefault() {
765 return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0;
766 },
767
768 toggleRememberChoice: function (aCheckbox) {
769 this.dialogElement("settingsChange").hidden = !aCheckbox.checked;
770 this.mDialog.sizeToContent();
771 },
772
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 },
780
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 }
800
801 // Enable Ok button if ok to press.
802 this.mDialog.document.documentElement.getButton("accept").disabled = !ok;
803 },
804
805 // Returns true iff the user-specified helper app has been modified.
806 appChanged: function() {
807 return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler;
808 },
809
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;
816
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);
843
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;
851
852 // Make sure mime info has updated setting for the "always ask" flag.
853 this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked;
854
855 return needUpdate && !discardUpdate;
856 },
857
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 },
865
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);
877
878 // Disable the OK button.
879 this.mDialog.document.documentElement.getButton("accept").disabled = true;
880 this.dialogElement("mode").focus();
881
882 // Clear chosen application.
883 this.chosenApp = null;
884
885 // Leave dialog up.
886 return false;
887 }
888 }
889
890 // Remove our web progress listener (a progress dialog will be
891 // taking over).
892 this.mLauncher.setWebProgressListener(null);
893
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();
900
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.
905
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) { }
917
918 this.mLauncher.saveToDisk(targetFile, false);
919 #endif
920
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);
930
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) { }
939
940 // Unhook dialog from this object.
941 this.mDialog.dialog = null;
942
943 // Close up dialog by returning true.
944 return true;
945 },
946
947 // onCancel:
948 onCancel: function() {
949 // Remove our web progress listener.
950 this.mLauncher.setWebProgressListener(null);
951
952 // Cancel app launcher.
953 try {
954 this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED);
955 } catch(exception) {
956 }
957
958 // Unhook dialog from this object.
959 this.mDialog.dialog = null;
960
961 // Close up dialog by returning true.
962 return true;
963 },
964
965 // dialogElement: Convenience.
966 dialogElement: function(id) {
967 return this.mDialog.document.getElementById(id);
968 },
969
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 },
989
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 }
999
1000 // Try to use the pretty description of the type, if one is available.
1001 var typeString = this.mLauncher.MIMEInfo.description;
1002
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;
1013 }
1014 }
1015
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;
1023
1024 this.mDialog.openDialog("chrome://global/content/appPicker.xul", null,
1025 "chrome,modal,centerscreen,titlebar,dialog=yes",
1026 params);
1027
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;
1033
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);
1041
1042 fp.appendFilters(nsIFilePicker.filterApps);
1043
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
1052
1053 // Show the "handler" menulist since we have a (user-specified)
1054 // application now.
1055 this.dialogElement("modeDeck").setAttribute("selectedIndex", "0");
1056
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");
1064
1065 this.dialogElement("mode").selectedItem = this.dialogElement("open");
1066 }
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);
1073 }
1074 },
1075
1076 // Turn this on to get debugging messages.
1077 debug: false,
1078
1079 // Dump text (if debug is on).
1080 dump: function( text ) {
1081 if ( this.debug ) {
1082 dump( text );
1083 }
1084 },
1085
1086 // dumpObj:
1087 dumpObj: function( spec ) {
1088 var val = "<undefined>";
1089 try {
1090 val = eval( "this."+spec ).toString();
1091 } catch( exception ) {
1092 }
1093 this.dump( spec + "=" + val + "\n" );
1094 },
1095
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 ) {
1104 }
1105 this.dump( val + "\n" );
1106 }
1107 }
1108 }
1109
1110 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsUnknownContentTypeDialog]);

mercurial