download-manager: Prevent unintended automatic opening of saved files with Gecko...
[conkeror.git] / modules / download-manager.js
blob7a604fbbcd1899fcc49f63124e7120204cedab2f
1 /**
2  * (C) Copyright 2008 Jeremy Maitin-Shepard
3  * (C) Copyright 2009 John Foerch
4  *
5  * Use, modification, and distribution are subject to the terms specified in the
6  * COPYING file.
7 **/
9 require("special-buffer.js");
10 require("mime-type-override.js");
11 require("minibuffer-read-mime-type.js");
12 require("compat/Map.js");
14 var download_manager_service = Cc["@mozilla.org/download-manager;1"]
15     .getService(Ci.nsIDownloadManager);
17 var unmanaged_download_info_list = [];
19 var id_to_download_info = new Map();
21 try {
22     Components.utils.import("resource://gre/modules/Downloads.jsm");
23     var use_downloads_jsm = true;
25     function lookup_download(download) {
26         return id_to_download_info.get(download);
27     }
28 } catch (e) {
29     var use_downloads_jsm = false;
31     function lookup_download(download) {
32         return id_to_download_info.get(download.id);
33     }
37 // Import these constants for convenience
38 const DOWNLOAD_NOTSTARTED = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
39 const DOWNLOAD_DOWNLOADING = Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING;
40 const DOWNLOAD_FINISHED = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
41 const DOWNLOAD_FAILED = Ci.nsIDownloadManager.DOWNLOAD_FAILED;
42 const DOWNLOAD_CANCELED = Ci.nsIDownloadManager.DOWNLOAD_CANCELED;
43 const DOWNLOAD_PAUSED = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
44 const DOWNLOAD_QUEUED = Ci.nsIDownloadManager.DOWNLOAD_QUEUED;
45 const DOWNLOAD_BLOCKED = Ci.nsIDownloadManager.DOWNLOAD_BLOCKED;
46 const DOWNLOAD_SCANNING = Ci.nsIDownloadManager.DOWNLOAD_SCANNING;
49 const DOWNLOAD_NOT_TEMPORARY = 0;
50 const DOWNLOAD_TEMPORARY_FOR_ACTION = 1;
51 const DOWNLOAD_TEMPORARY_FOR_COMMAND = 2;
53 function download_info (source_buffer, mozilla_info, target_file) {
54     this.source_buffer = source_buffer;
55     this.target_file = target_file;
56     if (mozilla_info != null)
57         this.attach(mozilla_info);
59 download_info.prototype = {
60     constructor: download_info,
61     target_file: null,
62     shell_command: null,
63     shell_command_cwd: null,
64     temporary_status: DOWNLOAD_NOT_TEMPORARY,
65     action_description: null,
66     set_shell_command: function (str, cwd) {
67         this.shell_command = str;
68         this.shell_command_cwd = cwd;
69         if (this.mozilla_info)
70             download_shell_command_change_hook.run(this);
71     },
73     /**
74      * None of the following members may be used until attach is called
75      */
77     // Reflectors to properties of nsIDownload
78     get source () { return this.mozilla_info.source; },
79     get id () { return this.mozilla_info.id; },
80     get referrer () { return this.mozilla_info.referrer; },
82     target_file_text: function () {
83         let target = this.target_file.path;
84         let display = this.display_name;
85         if (target.indexOf(display, target.length - display.length) == -1)
86             target += " (" + display + ")";
87         return target;
88     },
90     throw_if_removed: function () {
91         if (this.removed)
92             throw interactive_error("Download has already been removed from the download manager.");
93     },
95     throw_state_error: function () {
96         switch (this.state) {
97         case DOWNLOAD_DOWNLOADING:
98             throw interactive_error("Download is already in progress.");
99         case DOWNLOAD_FINISHED:
100             throw interactive_error("Download has already completed.");
101         case DOWNLOAD_FAILED:
102             throw interactive_error("Download has already failed.");
103         case DOWNLOAD_CANCELED:
104             throw interactive_error("Download has already been canceled.");
105         case DOWNLOAD_PAUSED:
106             throw interactive_error("Download has already been paused.");
107         case DOWNLOAD_QUEUED:
108             throw interactive_error("Download is queued.");
109         default:
110             throw new Error("Download has unexpected state: " + this.state);
111         }
112     },
114     // Download manager operations
115     cancel: function () {
116         this.throw_if_removed();
117         switch (this.state) {
118         case DOWNLOAD_DOWNLOADING:
119         case DOWNLOAD_PAUSED:
120         case DOWNLOAD_QUEUED:
121             if (use_downloads_jsm) {
122                 yield this.mozilla_info.finalize(true);
123             } else {
124                 try {
125                     download_manager_service.cancelDownload(this.id);
126                 } catch (e) {
127                     throw interactive_error("Download cannot be canceled.");
128                 }
129             }
130             break;
131         default:
132             this.throw_state_error();
133         }
134     },
136     retry: function () {
137         this.throw_if_removed();
138         switch (this.state) {
139         case DOWNLOAD_CANCELED:
140         case DOWNLOAD_FAILED:
141             if (use_downloads_jsm) {
142                 yield this.mozilla_info.start();
143             } else {
144                 try {
145                     download_manager_service.retryDownload(this.id);
146                 } catch (e) {
147                     throw interactive_error("Download cannot be retried.");
148                 }
149             }
150             break;
151         default:
152             this.throw_state_error();
153         }
154     },
156     resume: function () {
157         this.throw_if_removed();
158         switch (this.state) {
159         case DOWNLOAD_PAUSED:
160             if (use_downloads_jsm) {
161                 yield this.mozilla_info.start();
162             } else {
164                 try {
165                     download_manager_service.resumeDownload(this.id);
166                 } catch (e) {
167                     throw interactive_error("Download cannot be resumed.");
168                 }
169             }
170             break;
171         default:
172             this.throw_state_error();
173         }
174     },
176     pause: function () {
177         this.throw_if_removed();
178         switch (this.state) {
179         case DOWNLOAD_DOWNLOADING:
180         case DOWNLOAD_QUEUED:
181             if (use_downloads_jsm) {
182                 yield this.mozilla_info.cancel();
183             } else {
184                 try {
185                     download_manager_service.pauseDownload(this.id);
186                 } catch (e) {
187                     throw interactive_error("Download cannot be paused.");
188                 }
189             }
190             break;
191         default:
192             this.throw_state_error();
193         }
194     },
196     remove: function () {
197         this.throw_if_removed();
198         switch (this.state) {
199         case DOWNLOAD_FAILED:
200         case DOWNLOAD_CANCELED:
201         case DOWNLOAD_FINISHED:
202             if (use_downloads_jsm) {
203                 let list = yield Downloads.getList(Downloads.ALL);
204                 yield list.remove(this.mozilla_info);
205             } else {
206                 try {
207                     download_manager_service.removeDownload(this.id);
208                 } catch (e) {
209                     throw interactive_error("Download cannot be removed.");
210                 }
211             }
212                 break;
213         default:
214             throw interactive_error("Download is still in progress.");
215         }
216     },
218     delete_target: function () {
219         if (this.state != DOWNLOAD_FINISHED)
220             throw interactive_error("Download has not finished.");
221         try {
222             this.target_file.remove(false);
223         } catch (e) {
224             if ("result" in e) {
225                 switch (e.result) {
226                 case Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
227                     throw interactive_error("File has already been deleted.");
228                 case Cr.NS_ERROR_FILE_ACCESS_DENIED:
229                     throw interactive_error("Access denied");
230                 case Cr.NS_ERROR_FILE_DIR_NOT_EMPTY:
231                     throw interactive_error("Failed to delete file.");
232                 }
233             }
234             throw e;
235         }
236     }
239 if (!use_downloads_jsm) {
240     download_info.prototype.__proto__ = {
241         attach: function (mozilla_info, existing) {
242             if (!this.target_file)
243                 this.__defineGetter__("target_file", function () {
244                     return this.mozilla_info.targetFile;
245                 });
246             else if (this.target_file.path != mozilla_info.targetFile.path)
247                 throw interactive_error("Download target file unexpected.");
249             this.mozilla_info = mozilla_info;
251             if (use_downloads_jsm) {
252                 id_to_download_info.set(mozilla_info, this);
253             } else {
254                 id_to_download_info.set(mozilla_info.id, this);
255             }
257             if (existing)
258                 existing_download_added_hook.run(this);
259             else
260                 download_added_hook.run(this);
261         },
262         get source_uri_string () { return this.mozilla_info.source.spec; },
263         get source_uri () { return this.mozilla_info.source; },
264         get display_name () { return this.mozilla_info.displayName; },
265         get amount_transferred () { return this.mozilla_info.amountTransferred; },
266         get percent_complete () { return this.mozilla_info.percentComplete; },
267         get speed () { return this.mozilla_info.speed; },
268         get state () { return this.mozilla_info.state; },
269         get start_time () { return this.mozilla_info.startTime / 1000; },
270         get MIME_info () { return this.mozilla_info.MIMEInfo; },
271         get MIME_type () {
272             if (this.MIME_info)
273                 return this.MIME_info.MIMEType;
274             return null;
275         },
276         get size () {
277             var s = this.mozilla_info.size;
278             /* nsIDownload.size is a PRUint64, and will have value
279              * LL_MAXUINT (2^64 - 1) to indicate an unknown size.  Because
280              * JavaScript only has a double numerical type, this value
281              * cannot be represented exactly, so 2^36 is used instead as the cutoff. */
282             if (s < 68719476736 /* 2^36 */)
283                 return s;
284             return -1;
285         },
286     };
287 } else {
288     download_info.prototype.__proto__ = {
289         attach: function (mozilla_info, existing) {
290             if (!this.target_file)
291                 this.__defineGetter__("target_file", function () {
292                     return make_file(this.mozilla_info.target.path);
293                 });
294             else if (this.target_file.path != mozilla_info.target.path)
295                 throw interactive_error("Download target file unexpected.");
297             this.mozilla_info = mozilla_info;
298             id_to_download_info.set(mozilla_info, this);
300             if (existing)
301                 existing_download_added_hook.run(this);
302             else
303                 download_added_hook.run(this);
304         },
305         get source_uri () { return make_uri(this.mozilla_info.source.url); },
306         get source_uri_string () { return this.mozilla_info.source.url; },
307         get display_name () { return this.mozilla_info.target.path; },
308         get amount_transferred () { return this.mozilla_info.currentBytes; },
309         get percent_complete () { return this.mozilla_info.progress; },
310         get speed () { return 1000 * this.amount_transferred / (Date.now() - this.start_time); },
311         get start_time () { return this.mozilla_info.startTime.getTime(); },
312         get MIME_type () { return this.mozilla_info.contentType; },
313         get state () {
314             if (this.mozilla_info.succeeded)
315                 return DOWNLOAD_FINISHED;
316             if (this.mozilla_info.canceled)
317                 return DOWNLOAD_CANCELED;
318             if (this.mozilla_info.error)
319                 return DOWNLOAD_FAILED;
320             if (!this.mozilla_info.startTime)
321                 return DOWNLOAD_NOTSTARTED;
322             return DOWNLOAD_DOWNLOADING;
323         },
324         get size () {
325             if (this.mozilla_info.hasProgress)
326                 return this.mozilla_info.totalBytes;
327             return -1;
328         },
329     };
332 var define_download_local_hook = simple_local_hook_definer();
334 function register_download (buffer, source_uri, target_file) {
335     var info = new download_info(buffer, null, target_file);
336     info.registered_time_stamp = Date.now();
337     info.registered_source_uri = source_uri;
338     unmanaged_download_info_list.push(info);
339     return info;
342 function match_registered_download (source) {
343     let list = unmanaged_download_info_list;
344     let t = Date.now();
345     for (let i = 0; i < list.length; ++i) {
346         let x = list[i];
347         if (x.registered_source_uri.spec == source) {
348             list.splice(i, 1);
349             return x;
350         }
351         if (t - x.registered_time_stamp > download_info_max_queue_delay) {
352             list.splice(i, 1);
353             --i;
354             continue;
355         }
356     }
357     return null;
360 define_download_local_hook("existing_download_added_hook");
361 define_download_local_hook("download_added_hook");
362 define_download_local_hook("download_removed_hook");
363 define_download_local_hook("download_finished_hook");
364 define_download_local_hook("download_progress_change_hook");
365 define_download_local_hook("download_state_change_hook");
366 define_download_local_hook("download_shell_command_change_hook");
368 define_variable('delete_temporary_files_for_command', true,
369     'If this is set to true, temporary files downloaded to run a command '+
370     'on them will be deleted once the command completes. If not, the file '+
371     'will stay around forever unless deleted outside the browser.');
373 var download_info_max_queue_delay = 100;
375 if (!use_downloads_jsm) {
377     var download_progress_listener = {
378         QueryInterface: generate_QI(Ci.nsIDownloadProgressListener),
380         onDownloadStateChange: function (state, download) {
381             var info = null;
382             /* FIXME: Determine if only new downloads will have this state
383              * as their previous state. */
385             dumpln("download state change: " + download.source.spec + ": " + state + ", " + download.state + ", " + download.id);
387             if (state == DOWNLOAD_NOTSTARTED) {
388                 info = match_registered_download(download.source.spec);
389                 if (info == null) {
390                     info = new download_info(null, download);
391                     dumpln("error: encountered unknown new download");
392                 } else {
393                     info.attach(download);
394                 }
395             } else {
396                 info = id_to_download_info.get(download.id);
397                 if (info == null) {
398                     dumpln("Error: encountered unknown download");
400                 } else {
401                     info.mozilla_info = download;
402                     download_state_change_hook.run(info);
403                     if (info.state == DOWNLOAD_FINISHED) {
404                         download_finished_hook.run(info);
406                         if (info.shell_command != null) {
407                             info.running_shell_command = true;
408                             spawn(function () {
409                                 try {
410                                     yield shell_command_with_argument(info.shell_command,
411                                                                       info.target_file.path,
412                                                                       $cwd = info.shell_command_cwd);
413                                 } catch (e) {
414                                     handle_interactive_error(info.source_buffer.window, e);
415                                 } finally  {
416                                     if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
417                                         if(delete_temporary_files_for_command) {
418                                             info.target_file.remove(false /* not recursive */);
419                                         }
420                                     info.running_shell_command = false;
421                                     download_shell_command_change_hook.run(info);
422                                 }
423                             }());
424                             download_shell_command_change_hook.run(info);
425                         }
426                     }
427                 }
428             }
429         },
431         onProgressChange: function (progress, request, cur_self_progress, max_self_progress,
432                                     cur_total_progress, max_total_progress,
433                                     download) {
434             var info = id_to_download_info.get(download.id);
435             if (info == null) {
436                 dumpln("error: encountered unknown download in progress change");
437                 return;
438             }
439             info.mozilla_info = download;
440             download_progress_change_hook.run(info);
441             //dumpln("download progress change: " + download.source.spec + ": " + cur_self_progress + "/" + max_self_progress + " "
442             // + cur_total_progress + "/" + max_total_progress + ", " + download.state + ", " + download.id);
443         },
445         onSecurityChange: function (progress, request, state, download) {
446         },
448         onStateChange: function (progress, request, state_flags, status, download) {
449         }
450     };
452     var download_observer = {
453         observe: function (subject, topic, data) {
454             switch(topic) {
455             case "download-manager-remove-download":
456                 var ids = [];
457                 if (!subject) {
458                     // Remove all downloads
459                     for (let i in id_to_download_info)
460                         ids.push(i);
461                 } else {
462                     let id = subject.QueryInterface(Ci.nsISupportsPRUint32);
463                     /* FIXME: determine if this should really be an error */
464                     if (!(id in id_to_download_info)) {
465                         dumpln("Error: download-manager-remove-download event received for unknown download: " + id);
466                     } else
467                         ids.push(id);
468                 }
469                 for each (let i in ids) {
470                     dumpln("deleting download: " + i);
471                     let d = id_to_download_info[i];
472                     d.removed = true;
473                     download_removed_hook.run(d);
474                     id_to_download_info.delete(i);
475                 }
476                 break;
477             }
478         }
479     };
481     observer_service.addObserver(download_observer, "download-manager-remove-download", false);
483     try {
484         download_manager_service.addListener(download_progress_listener);
485     } catch (e) {
486         dumpln("Failed to register download progress listener.");
487         dump_error(e);
488     }
489 } else {
491     spawn(function() {
492         let list = yield Downloads.getList(Downloads.ALL);
493         let view = {
494             onDownloadAdded: function (download) {
495                 // We never want the automatic launching to be used
496                 // This is set by default when using nsIWebBrowserPersist
497                 download.launchWhenSucceeded = false;
499                 let info = match_registered_download(download.source.url);
501                 if (info == null) {
502                     info = new download_info(null, download);
503                     dumpln("Encountered unknown new download");
504                 } else {
505                     info.attach(download);
506                 }
507             },
508             onDownloadChanged: function (download) {
509                 let info = lookup_download(download);
510                 if (!info)
511                     dumpln("error: onDownloadChanged: encountered unknown download");
512                 else {
513                     download_progress_change_hook.run(info);
514                     download_state_change_hook.run(info);
516                     if (info.state == DOWNLOAD_FINISHED) {
517                         download_finished_hook.run(info);
519                         if (info.shell_command != null) {
520                             info.running_shell_command = true;
521                             spawn(function () {
522                                 try {
523                                     yield shell_command_with_argument(info.shell_command,
524                                                                       info.target_file.path,
525                                                                       $cwd = info.shell_command_cwd);
526                                 } catch (e) {
527                                     handle_interactive_error(info.source_buffer.window, e);
528                                 } finally  {
529                                     if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
530                                         if(delete_temporary_files_for_command) {
531                                             info.target_file.remove(false /* not recursive */);
532                                         }
533                                     info.running_shell_command = false;
534                                     download_shell_command_change_hook.run(info);
535                                 }
536                             }());
537                             download_shell_command_change_hook.run(info);
538                         }
539                     }
540                 }
541             },
542             onDownloadRemoved: function (download) {
543                 let info = lookup_download(download);
544                 if (!info)
545                     dumpln("error: onDownloadRemoved: encountered unknown download");
546                 else {
547                     dumpln("Removing download: " + info.source_uri_string);
548                     info.removed = true;
549                     download_removed_hook.run(info);
550                     id_to_download_info.delete(download);
551                 }
552             }
553         };
554         list.addView(view);
555     }());
558 define_variable("download_buffer_min_update_interval", 2000,
559     "Minimum interval (in milliseconds) between updates in download progress buffers.\n" +
560     "Lowering this interval will increase the promptness of the progress display at " +
561     "the cost of using additional processor time.");
563 function download_buffer_modality (buffer, element) {
564     buffer.keymaps.push(download_buffer_keymap);
567 define_keywords("$info");
568 function download_buffer (window) {
569     this.constructor_begin();
570     keywords(arguments);
571     special_buffer.call(this, window, forward_keywords(arguments));
572     this.info = arguments.$info;
573     this.local.cwd = this.info.target_file.parent;
574     this.description = this.info.source_uri_string;
575     this.update_title();
577     this.progress_change_handler_fn = method_caller(this, this.handle_progress_change);
579     // With Downloads.jsm integration, download_progress_change_hook is redundant with download_state_change_hook
580     if (!use_downloads_jsm)
581         add_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
583     add_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
584     this.command_change_handler_fn = method_caller(this, this.update_command_field);
585     add_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
586     this.modalities.push(download_buffer_modality);
587     this.constructor_end();
589 download_buffer.prototype = {
590     constructor: download_buffer,
591     __proto__: special_buffer.prototype,
592     toString: function () "#<download_buffer>",
594     destroy: function () {
595         if (!use_downloads_jsm)
596             remove_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
598         remove_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
599         remove_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
601         // Remove all node references
602         delete this.status_textnode;
603         delete this.target_file_node;
604         delete this.transferred_div_node;
605         delete this.transferred_textnode;
606         delete this.progress_container_node;
607         delete this.progress_bar_node;
608         delete this.percent_textnode;
609         delete this.time_textnode;
610         delete this.command_div_node;
611         delete this.command_label_textnode;
612         delete this.command_textnode;
614         special_buffer.prototype.destroy.call(this);
615     },
617     update_title: function () {
618         try {
619             // FIXME: do this properly
620             var new_title;
621             var info = this.info;
622             var append_transfer_info = false;
623             var append_speed_info = true;
624             var label = null;
625             switch(info.state) {
626             case DOWNLOAD_DOWNLOADING:
627                 label = "Downloading";
628                 append_transfer_info = true;
629                 break;
630             case DOWNLOAD_FINISHED:
631                 label = "Download complete";
632                 break;
633             case DOWNLOAD_FAILED:
634                 label = "Download failed";
635                 append_transfer_info = true;
636                 append_speed_info = false;
637                 break;
638             case DOWNLOAD_CANCELED:
639                 label = "Download canceled";
640                 append_transfer_info = true;
641                 append_speed_info = false;
642                 break;
643             case DOWNLOAD_PAUSED:
644                 label = "Download paused";
645                 append_transfer_info = true;
646                 append_speed_info = false;
647                 break;
648             case DOWNLOAD_QUEUED:
649             default:
650                 label = "Download queued";
651                 break;
652             }
654             if (append_transfer_info) {
655                 if (append_speed_info)
656                     new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
657                 else
658                     new_title = label + ": ";
659                 var trans = pretty_print_file_size(info.amount_transferred);
660                 if (info.size >= 0) {
661                     var total = pretty_print_file_size(info.size);
662                     if (trans[1] == total[1])
663                         new_title += trans[0] + "/" + total[0] + " " + total[1];
664                     else
665                         new_title += trans.join(" ") + "/" + total.join(" ");
666                 } else
667                     new_title += trans.join(" ");
668                 if (info.percent_complete >= 0)
669                     new_title += " (" + info.percent_complete + "%)";
670             } else
671                 new_title = label;
672             if (new_title != this.title) {
673                 this.title = new_title;
674                 return true;
675             }
676             return false;
677         } catch (e) {
678             dump_error(e);
679             throw e;
680         }
681     },
683     handle_progress_change: function () {
684         var cur_time = Date.now();
685         if (this.last_update == null ||
686             (cur_time - this.last_update) > download_buffer_min_update_interval ||
687             this.info.state != this.previous_state) {
689             if (this.update_title())
690                 buffer_title_change_hook.run(this);
692             if (this.generated) {
693                 this.update_fields();
694             }
695             this.previous_status = this.info.status;
696             this.last_update = cur_time;
697         }
698     },
700     generate: function () {
701         var d = this.document;
702         var g = new dom_generator(d, XHTML_NS);
704         /* Warning: If any additional node references are saved in
705          * this function, appropriate code to delete the saved
706          * properties must be added to destroy method. */
708         var info = this.info;
710         d.body.setAttribute("class", "download-buffer");
712         g.add_stylesheet("chrome://conkeror-gui/content/downloads.css");
714         var row, cell;
715         var table = g.element("table", d.body);
717         row = g.element("tr", table, "class", "download-info", "id", "download-source");
718         cell = g.element("td", row, "class", "download-label");
719         this.status_textnode = g.text("", cell);
720         cell = g.element("td", row, "class", "download-value");
721         g.text(info.source_uri_string, cell);
723         row = g.element("tr", table, "class", "download-info", "id", "download-target");
724         cell = g.element("td", row, "class", "download-label");
725         var target_label;
726         if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
727             target_label = "Temp. file:";
728         else
729             target_label = "Target:";
730         g.text(target_label, cell);
731         cell = g.element("td", row, "class", "download-value");
732         this.target_file_node = g.text("", cell);
734         row = g.element("tr", table, "class", "download-info", "id", "download-mime-type");
735         cell = g.element("td", row, "class", "download-label");
736         g.text("MIME type:", cell);
737         cell = g.element("td", row, "class", "download-value");
738         g.text(info.MIME_type || "unknown", cell);
740         this.transferred_div_node = row =
741             g.element("tr", table, "class", "download-info", "id", "download-transferred");
742         cell = g.element("td", row, "class", "download-label");
743         g.text("Transferred:", cell);
744         cell = g.element("td", row, "class", "download-value");
745         var sub_item = g.element("div", cell);
746         this.transferred_textnode = g.text("", sub_item);
747         sub_item = g.element("div", cell, "id", "download-percent");
748         this.percent_textnode = g.text("", sub_item);
749         this.progress_container_node = sub_item = g.element("div", cell, "id", "download-progress-container");
750         this.progress_bar_node = g.element("div", sub_item, "id", "download-progress-bar");
752         row = g.element("tr", table, "class", "download-info", "id", "download-time");
753         cell = g.element("td", row, "class", "download-label");
754         g.text("Time:", cell);
755         cell = g.element("td", row, "class", "download-value");
756         this.time_textnode = g.text("", cell);
758         if (info.action_description != null) {
759             row = g.element("tr", table, "class", "download-info", "id", "download-action");
760             cell = g.element("div", row, "class", "download-label");
761             g.text("Action:", cell);
762             cell = g.element("div", row, "class", "download-value");
763             g.text(info.action_description, cell);
764         }
766         this.command_div_node = row = g.element("tr", table, "class", "download-info", "id", "download-command");
767         cell = g.element("td", row, "class", "download-label");
768         this.command_label_textnode = g.text("Run command:", cell);
769         cell = g.element("td", row, "class", "download-value");
770         this.command_textnode = g.text("", cell);
772         this.update_fields();
773         this.update_command_field();
774     },
776     update_fields: function () {
777         if (!this.generated)
778             return;
779         var info = this.info;
780         var label = null;
781         switch (info.state) {
782         case DOWNLOAD_DOWNLOADING:
783             label = "Downloading";
784             break;
785         case DOWNLOAD_FINISHED:
786             label = "Completed";
787             break;
788         case DOWNLOAD_FAILED:
789             label = "Failed";
790             break;
791         case DOWNLOAD_CANCELED:
792             label = "Canceled";
793             break;
794         case DOWNLOAD_PAUSED:
795             label = "Paused";
796             break;
797         case DOWNLOAD_QUEUED:
798         default:
799             label = "Queued";
800             break;
801         }
802         this.status_textnode.nodeValue = label + ":";
803         this.target_file_node.nodeValue = info.target_file_text();
804         this.update_time_field();
806         var tran_text = "";
807         if (info.state == DOWNLOAD_FINISHED)
808             tran_text = pretty_print_file_size(info.size).join(" ");
809         else {
810             var trans = pretty_print_file_size(info.amount_transferred);
811             if (info.size >= 0) {
812                 var total = pretty_print_file_size(info.size);
813                 if (trans[1] == total[1])
814                     tran_text += trans[0] + "/" + total[0] + " " + total[1];
815                 else
816                     tran_text += trans.join(" ") + "/" + total.join(" ");
817             } else
818                 tran_text += trans.join(" ");
819         }
820         this.transferred_textnode.nodeValue = tran_text;
821         if (info.percent_complete >= 0) {
822             this.progress_container_node.style.display = "";
823             this.percent_textnode.nodeValue = info.percent_complete + "%";
824             this.progress_bar_node.style.width = info.percent_complete + "%";
825         } else {
826             this.percent_textnode.nodeValue = "";
827             this.progress_container_node.style.display = "none";
828         }
830         this.update_command_field();
831     },
833     update_time_field: function () {
834         var info = this.info;
835         var elapsed_text = pretty_print_time((Date.now() - info.start_time) / 1000) + " elapsed";
836         var text = "";
837         if (info.state == DOWNLOAD_DOWNLOADING)
838             text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
839         if (info.state == DOWNLOAD_DOWNLOADING &&
840             info.size >= 0 &&
841             info.speed > 0)
842         {
843             let remaining = (info.size - info.amount_transferred) / info.speed;
844             text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
845         } else
846             text = elapsed_text;
847         this.time_textnode.nodeValue = text;
848     },
850     update_command_field: function () {
851         if (!this.generated)
852             return;
853         if (this.info.shell_command != null) {
854             this.command_div_node.style.display = "";
855             var label;
856             if (this.info.running_shell_command)
857                 label = "Running:";
858             else if (this.info.state == DOWNLOAD_FINISHED)
859                 label = "Ran command:";
860             else
861                 label = "Run command:";
862             this.command_label_textnode.nodeValue = label;
863             this.command_textnode.nodeValue = this.info.shell_command;
864         } else
865             this.command_div_node.style.display = "none";
866     }
869 function download_cancel (buffer) {
870     check_buffer(buffer, download_buffer);
871     var info = buffer.info;
872     yield info.cancel();
873     buffer.window.minibuffer.message("Download canceled");
875 interactive("download-cancel",
876     "Cancel the current download.\n" +
877     "The download can later be retried using the `download-retry' "+
878     "command, but any data already transferred will be lost.",
879     function (I) {
880         let result = yield I.window.minibuffer.read_single_character_option(
881             $prompt = "Cancel this download? (y/n)",
882             $options = ["y", "n"]);
883         if (result == "y")
884             yield download_cancel(I.buffer);
885     });
887 function download_retry (buffer) {
888     check_buffer(buffer, download_buffer);
889     var info = buffer.info;
890     yield info.retry();
891     buffer.window.minibuffer.message("Download retried");
893 interactive("download-retry",
894     "Retry a failed or canceled download.\n" +
895     "This command can be used to retry a download that failed or "+
896     "was canceled using the `download-cancel' command.  The download "+
897     "will begin from the start again.",
898     function (I) { yield download_retry(I.buffer); });
900 function download_pause (buffer) {
901     check_buffer(buffer, download_buffer);
902     yield buffer.info.pause();
903     buffer.window.minibuffer.message("Download paused");
905 interactive("download-pause",
906     "Pause the current download.\n" +
907     "The download can later be resumed using the `download-resume' command. "+
908     "The data already transferred will not be lost.",
909     function (I) { yield download_pause(I.buffer); });
911 function download_resume (buffer) {
912     check_buffer(buffer, download_buffer);
913     yield buffer.info.resume();
914     buffer.window.minibuffer.message("Download resumed");
916 interactive("download-resume",
917     "Resume the current download.\n" +
918     "This command can be used to resume a download paused using the "+
919     "`download-pause' command.",
920     function (I) { yield download_resume(I.buffer); });
922 function download_remove (buffer) {
923     check_buffer(buffer, download_buffer);
924     yield buffer.info.remove();
925     buffer.window.minibuffer.message("Download removed");
927 interactive("download-remove",
928     "Remove the current download from the download manager.\n" +
929     "This command can only be used on inactive (paused, canceled, "+
930     "completed, or failed) downloads.",
931     function (I) { yield download_remove(I.buffer); });
933 function download_retry_or_resume (buffer) {
934     check_buffer(buffer, download_buffer);
935     var info = buffer.info;
936     if (info.state == DOWNLOAD_PAUSED)
937         yield download_resume(buffer);
938     else
939         yield download_retry(buffer);
941 interactive("download-retry-or-resume",
942     "Retry or resume the current download.\n" +
943     "This command can be used to resume a download paused using the " +
944     "`download-pause' command or canceled using the `download-cancel' "+
945     "command.",
946     function (I) { yield download_retry_or_resume(I.buffer); });
948 function download_pause_or_resume (buffer) {
949     check_buffer(buffer, download_buffer);
950     var info = buffer.info;
951     if (info.state == DOWNLOAD_PAUSED)
952         yield download_resume(buffer);
953     else
954         yield download_pause(buffer);
956 interactive("download-pause-or-resume",
957     "Pause or resume the current download.\n" +
958     "This command toggles the paused state of the current download.",
959     function (I) { yield download_pause_or_resume(I.buffer); });
961 function download_delete_target (buffer) {
962     check_buffer(buffer, download_buffer);
963     var info = buffer.info;
964     info.delete_target();
965     buffer.window.minibuffer.message("Deleted file: " + info.target_file.path);
967 interactive("download-delete-target",
968     "Delete the target file of the current download.\n" +
969     "This command can only be used if the download has finished successfully.",
970     function (I) { download_delete_target(I.buffer); });
972 function download_shell_command (buffer, cwd, cmd) {
973     check_buffer(buffer, download_buffer);
974     var info = buffer.info;
975     if (info.state == DOWNLOAD_FINISHED) {
976         shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd);
977         return;
978     }
979     if (info.state != DOWNLOAD_DOWNLOADING &&
980         info.state != DOWNLOAD_PAUSED &&
981         info.state != DOWNLOAD_QUEUED)
982     {
983         info.throw_state_error();
984     }
985     if (cmd == null || cmd.length == 0)
986         info.set_shell_command(null, cwd);
987     else
988         info.set_shell_command(cmd, cwd);
989     buffer.window.minibuffer.message("Queued shell command: " + cmd);
991 interactive("download-shell-command",
992     "Run a shell command on the target file of the current download.\n"+
993     "If the download is still in progress, the shell command will be queued "+
994     "to run when the download finishes.",
995     function (I) {
996         var buffer = check_buffer(I.buffer, download_buffer);
997         var cwd = buffer.info.shell_command_cwd || I.local.cwd;
998         var cmd = yield I.minibuffer.read_shell_command(
999             $cwd = cwd,
1000             $initial_value = buffer.info.shell_command ||
1001                 external_content_handlers.get(buffer.info.MIME_type));
1002         download_shell_command(buffer, cwd, cmd);
1003     });
1005 function download_manager_ui () {}
1006 download_manager_ui.prototype = {
1007     constructor: download_manager_ui,
1008     QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
1010     getAttention: function () {},
1011     show: function () {},
1012     visible: false
1016 interactive("download-manager-show-builtin-ui",
1017     "Show the built-in (Firefox-style) download manager window.",
1018     function (I) {
1019         Components.classesByID["{7dfdf0d1-aff6-4a34-bad1-d0fe74601642}"]
1020             .getService(Ci.nsIDownloadManagerUI)
1021             .show(I.window);
1022     });
1026  * Download-show
1027  */
1029 define_variable("download_temporary_file_open_buffer_delay", 500,
1030     "Delay (in milliseconds) before a download buffer is opened for "+
1031     "temporary downloads.  If the download completes before this amount "+
1032     "of time, no download buffer will be opened.  This variable takes "+
1033     "effect only if `open_download_buffer_automatically' is in "+
1034     "`download_added_hook', which is the case by default.");
1036 define_variable("download_buffer_automatic_open_target", OPEN_NEW_WINDOW,
1037     "Target(s) for download buffers created by "+
1038     "`open_download_buffer_automatically'.");
1040 minibuffer_auto_complete_preferences.download = true;
1042 function download_completer (completions) {
1043     keywords(arguments);
1044     if (! use_downloads_jsm) {
1045         completions = function (visitor) {
1046             var dls = download_manager_service.activeDownloads;
1047             while (dls.hasMoreElements()) {
1048                 let dl = dls.getNext();
1049                 visitor(dl);
1050             }
1051         };
1052     }
1053     all_word_completer.call(this, forward_keywords(arguments),
1054                             $completions = completions);
1056 download_completer.prototype = {
1057     constructor: download_completer,
1058     __proto__: all_word_completer.prototype,
1059     toString: function () "#<download_completer>",
1060     get_string: function (x) {
1061         if (use_downloads_jsm)
1062             return x.target.path;
1063         else
1064             return x.displayName;
1065     },
1066     get_description: function (x) {
1067         if (use_downloads_jsm)
1068             return x.source.url;
1069         else
1070             return x.source.spec
1071     }
1074 minibuffer.prototype.read_download = function () {
1075     keywords(arguments,
1076              $prompt = "Download",
1077              $auto_complete = "download",
1078              $auto_complete_initial = true,
1079              $require_match = true);
1080     if (use_downloads_jsm) {
1081         var list = yield Downloads.getList(Downloads.ALL);
1082         var all_downloads = yield list.getAll();
1083         var completer = new download_completer(all_downloads);
1084     } else {
1085         completer = new download_completer();
1086     }
1087     var result = yield this.read(forward_keywords(arguments),
1088                                  $completer = completer);
1089     yield co_return(result);
1093 function download_show (window, target, mozilla_info) {
1094     if (! window)
1095         target = OPEN_NEW_WINDOW;
1096     var info = lookup_download(mozilla_info);
1097     if (!info) {
1098         info = new download_info(null, null);
1099         info.attach(mozilla_info, true /* existing */);
1100     }
1101     create_buffer(window, buffer_creator(download_buffer, $info = info), target);
1104 function download_show_new_window (I) {
1105     var mozilla_info = yield I.minibuffer.read_download($prompt = "Show download:");
1106     download_show(I.window, OPEN_NEW_WINDOW, mozilla_info);
1109 function download_show_new_buffer (I) {
1110     var mozilla_info = yield I.minibuffer.read_download($prompt = "Show download:");
1111     download_show(I.window, OPEN_NEW_BUFFER, mozilla_info);
1114 function download_show_new_buffer_background (I) {
1115     var mozilla_info = yield I.minibuffer.read_download($prompt = "Show download:");
1116     download_show(I.window, OPEN_NEW_BUFFER_BACKGROUND, mozilla_info);
1119 function open_download_buffer_automatically (info) {
1120     var buf = info.source_buffer;
1121     var target = download_buffer_automatic_open_target;
1122     if (info.temporary_status == DOWNLOAD_NOT_TEMPORARY ||
1123         download_temporary_file_open_buffer_delay == 0)
1124     {
1125         download_show(buf ? buf.window : null, target, info.mozilla_info);
1126     } else {
1127         var timer = null;
1128         function finish () {
1129             timer.cancel();
1130         }
1131         add_hook.call(info, "download_finished_hook", finish);
1132         timer = call_after_timeout(function () {
1133             remove_hook.call(info, "download_finished_hook", finish);
1134             download_show(buf ? buf.window : null, target, info.mozilla_info);
1135         }, download_temporary_file_open_buffer_delay);
1136     }
1138 add_hook("download_added_hook", open_download_buffer_automatically);
1140 interactive("download-show",
1141     "Prompt for an ongoing download and open a download buffer showing "+
1142     "its progress.",
1143     alternates(download_show_new_buffer,
1144                download_show_new_window));
1146 provide("download-manager");