!import
1 //@line 44 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/toolkit/mozapps/downloads/content/downloads.js"
2
3 ////////////////////////////////////////////////////////////////////////////////
4 //// Globals
5
6 const PREF_BDM_CLOSEWHENDONE = "browser.download.manager.closeWhenDone";
7 const PREF_BDM_ALERTONEXEOPEN = "browser.download.manager.alertOnEXEOpen";
8
9 const nsLocalFile = Components.Constructor("@mozilla.org/file/local;1",
10 "nsILocalFile", "initWithPath");
11
12 var Cc = Components.classes;
13 var Ci = Components.interfaces;
14 let Cu = Components.utils;
15 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
16 Cu.import("resource://gre/modules/DownloadUtils.jsm");
17 Cu.import("resource://gre/modules/PluralForm.jsm");
18
19 const nsIDM = Ci.nsIDownloadManager;
20
21 let gDownloadManager = Cc["@mozilla.org/download-manager;1"].getService(nsIDM);
22 let gDownloadListener = null;
23 let gDownloadsView = null;
24 let gSearchBox = null;
25 let gSearchTerms = [];
26 let gBuilder = 0;
27
28 // This variable is used when performing commands on download items and gives
29 // the command the ability to do something after all items have been operated
30 // on. The following convention is used to handle the value of the variable:
31 // whenever we aren't performing a command, the value is |undefined|; just
32 // before executing commands, the value will be set to |null|; and when
33 // commands want to create a callback, they set the value to be a callback
34 // function to be executed after all download items have been visited.
35 let gPerformAllCallback;
36
37 // Control the performance of the incremental list building by setting how many
38 // milliseconds to wait before building more of the list and how many items to
39 // add between each delay.
40 const gListBuildDelay = 300;
41 const gListBuildChunk = 3;
42
43 // Array of download richlistitem attributes to check when searching
44 const gSearchAttributes = [
45 "target",
46 "status",
47 "dateTime",
48 ];
49
50 // If the user has interacted with the window in a significant way, we should
51 // not auto-close the window. Tough UI decisions about what is "significant."
52 var gUserInteracted = false;
53
54 // These strings will be converted to the corresponding ones from the string
55 // bundle on startup.
56 let gStr = {
57 paused: "paused",
58 cannotPause: "cannotPause",
59 doneStatus: "doneStatus",
60 doneSize: "doneSize",
61 doneSizeUnknown: "doneSizeUnknown",
62 stateFailed: "stateFailed",
63 stateCanceled: "stateCanceled",
64 stateBlockedParentalControls: "stateBlocked",
65 stateBlockedPolicy: "stateBlockedPolicy",
66 stateDirty: "stateDirty",
67 yesterday: "yesterday",
68 monthDate: "monthDate",
69 downloadsTitleFiles: "downloadsTitleFiles",
70 downloadsTitlePercent: "downloadsTitlePercent",
71 fileExecutableSecurityWarningTitle: "fileExecutableSecurityWarningTitle",
72 fileExecutableSecurityWarningDontAsk: "fileExecutableSecurityWarningDontAsk"
73 };
74
75 // The statement to query for downloads that are active or match the search
76 let gStmt = gDownloadManager.DBConnection.createStatement(
77 "SELECT id, target, name, source, state, startTime, endTime, referrer, " +
78 "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
79 "FROM moz_downloads " +
80 "ORDER BY isActive DESC, endTime DESC, startTime DESC");
81
82 ////////////////////////////////////////////////////////////////////////////////
83 //// Utility Functions
84
getDownload
85 function getDownload(aID)
86 {
87 return document.getElementById("dl" + aID);
88 }
89
90 ////////////////////////////////////////////////////////////////////////////////
91 //// Start/Stop Observers
92
downloadCompleted
93 function downloadCompleted(aDownload)
94 {
95 // Wrap this in try...catch since this can be called while shutting down...
96 // it doesn't really matter if it fails then since well.. we're shutting down
97 // and there's no UI to update!
98 try {
99 let dl = getDownload(aDownload.id);
100
101 // Update attributes now that we've finished
102 dl.setAttribute("startTime", Math.round(aDownload.startTime / 1000));
103 dl.setAttribute("endTime", Date.now());
104 dl.setAttribute("currBytes", aDownload.amountTransferred);
105 dl.setAttribute("maxBytes", aDownload.size);
106
107 // Move the download below active if it should stay in the list
108 if (downloadMatchesSearch(dl)) {
109 // Iterate down until we find a non-active download
110 let next = dl.nextSibling;
111 while (next && next.inProgress)
112 next = next.nextSibling;
113
114 // Move the item and color everything after where it moved from
115 let fixup = dl.nextSibling;
116 gDownloadsView.insertBefore(dl, next);
117 stripeifyList(fixup);
118 } else {
119 removeFromView(dl);
120 }
121
122 // getTypeFromFile fails if it can't find a type for this file.
123 try {
124 // Refresh the icon, so that executable icons are shown.
125 var mimeService = Cc["@mozilla.org/mime;1"].
126 getService(Ci.nsIMIMEService);
127 var contentType = mimeService.getTypeFromFile(aDownload.targetFile);
128
129 var listItem = getDownload(aDownload.id)
130 var oldImage = listItem.getAttribute("image");
131 // Tacking on contentType bypasses cache
132 listItem.setAttribute("image", oldImage + "&contentType=" + contentType);
133 } catch (e) { }
134
135 if (gDownloadManager.activeDownloadCount == 0)
136 document.title = document.documentElement.getAttribute("statictitle");
137 }
138 catch (e) { }
139 }
140
autoRemoveAndClose
141 function autoRemoveAndClose(aDownload)
142 {
143 var pref = Cc["@mozilla.org/preferences-service;1"].
144 getService(Ci.nsIPrefBranch);
145
146 if (gDownloadManager.activeDownloadCount == 0) {
147 // For the moment, just use the simple heuristic that if this window was
148 // opened by the download process, rather than by the user, it should
149 // auto-close if the pref is set that way. If the user opened it themselves,
150 // it should not close until they explicitly close it. Additionally, the
151 // preference to control the feature may not be set, so defaulting to
152 // keeping the window open.
153 let autoClose = false;
154 try {
155 autoClose = pref.getBoolPref(PREF_BDM_CLOSEWHENDONE);
156 } catch (e) { }
157 var autoOpened =
158 !window.opener || window.opener.location.href == window.location.href;
159 if (autoClose && autoOpened && !gUserInteracted) {
160 gCloseDownloadManager();
161 return true;
162 }
163 }
164
165 return false;
166 }
167
168 // This function can be overwritten by extensions that wish to place the
169 // Download Window in another part of the UI.
gCloseDownloadManager
170 function gCloseDownloadManager()
171 {
172 window.close();
173 }
174
175 ////////////////////////////////////////////////////////////////////////////////
176 //// Download Event Handlers
177
cancelDownload
178 function cancelDownload(aDownload)
179 {
180 gDownloadManager.cancelDownload(aDownload.getAttribute("dlid"));
181
182 // XXXben -
183 // If we got here because we resumed the download, we weren't using a temp file
184 // because we used saveURL instead. (this is because the proper download mechanism
185 // employed by the helper app service isn't fully accessible yet... should be fixed...
186 // talk to bz...)
187 // the upshot is we have to delete the file if it exists.
188 var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
189
190 if (f.exists())
191 f.remove(false);
192 }
193
pauseDownload
194 function pauseDownload(aDownload)
195 {
196 var id = aDownload.getAttribute("dlid");
197 gDownloadManager.pauseDownload(id);
198 }
199
resumeDownload
200 function resumeDownload(aDownload)
201 {
202 gDownloadManager.resumeDownload(aDownload.getAttribute("dlid"));
203 }
204
removeDownload
205 function removeDownload(aDownload)
206 {
207 gDownloadManager.removeDownload(aDownload.getAttribute("dlid"));
208 }
209
retryDownload
210 function retryDownload(aDownload)
211 {
212 removeFromView(aDownload);
213 gDownloadManager.retryDownload(aDownload.getAttribute("dlid"));
214 }
215
showDownload
216 function showDownload(aDownload)
217 {
218 var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
219
220 try {
221 // Show the directory containing the file and select the file
222 f.reveal();
223 } catch (e) {
224 // If reveal fails for some reason (e.g., it's not implemented on unix or
225 // the file doesn't exist), try using the parent if we have it.
226 let parent = f.parent.QueryInterface(Ci.nsILocalFile);
227 if (!parent)
228 return;
229
230 try {
231 // "Double click" the parent directory to show where the file should be
232 parent.launch();
233 } catch (e) {
234 // If launch also fails (probably because it's not implemented), let the
235 // OS handler try to open the parent
236 openExternal(parent);
237 }
238 }
239 }
240
onDownloadDblClick
241 function onDownloadDblClick(aEvent)
242 {
243 // Only do the default action for double primary clicks
244 if (aEvent.button == 0)
245 doDefaultForSelected();
246 }
247
openDownload
248 function openDownload(aDownload)
249 {
250 var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
251 if (f.isExecutable()) {
252 var dontAsk = false;
253 var pref = Cc["@mozilla.org/preferences-service;1"].
254 getService(Ci.nsIPrefBranch);
255 try {
256 dontAsk = !pref.getBoolPref(PREF_BDM_ALERTONEXEOPEN);
257 } catch (e) { }
258
259 if (!dontAsk) {
260 var strings = document.getElementById("downloadStrings");
261 var name = aDownload.getAttribute("target");
262 var message = strings.getFormattedString("fileExecutableSecurityWarning", [name, name]);
263
264 let title = gStr.fileExecutableSecurityWarningTitle;
265 let dontAsk = gStr.fileExecutableSecurityWarningDontAsk;
266
267 var promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
268 getService(Ci.nsIPromptService);
269 var checkbox = { value: false };
270 var open = promptSvc.confirmCheck(window, title, message, dontAsk, checkbox);
271
272 if (!open)
273 return;
274 pref.setBoolPref(PREF_BDM_ALERTONEXEOPEN, !checkbox.value);
275 }
276 }
277 try {
278 f.launch();
279 } catch (ex) {
280 // if launch fails, try sending it through the system's external
281 // file: URL handler
282 openExternal(f);
283 }
284 }
285
openReferrer
286 function openReferrer(aDownload)
287 {
288 openURL(getReferrerOrSource(aDownload));
289 }
290
copySourceLocation
291 function copySourceLocation(aDownload)
292 {
293 var uri = aDownload.getAttribute("uri");
294 var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
295 getService(Ci.nsIClipboardHelper);
296
297 // Check if we should initialize a callback
298 if (gPerformAllCallback === null) {
299 let uris = [];
anon:300:26
300 gPerformAllCallback = function(aURI) aURI ? uris.push(aURI) :
301 clipboard.copyString(uris.join("\n"));
302 }
303
304 // We have a callback to use, so use it to add a uri
305 if (typeof gPerformAllCallback == "function")
306 gPerformAllCallback(uri);
307 else {
308 // It's a plain copy source, so copy it
309 clipboard.copyString(uri);
310 }
311 }
312
313 // This is called by the progress listener.
314 var gLastComputedMean = -1;
315 var gLastActiveDownloads = 0;
onUpdateProgress
316 function onUpdateProgress()
317 {
318 let numActiveDownloads = gDownloadManager.activeDownloadCount;
319
320 // Use the default title and reset "last" values if there's no downloads
321 if (numActiveDownloads == 0) {
322 document.title = document.documentElement.getAttribute("statictitle");
323 gLastComputedMean = -1;
324 gLastActiveDownloads = 0;
325
326 return;
327 }
328
329 // Establish the mean transfer speed and amount downloaded.
330 var mean = 0;
331 var base = 0;
332 var dls = gDownloadManager.activeDownloads;
333 while (dls.hasMoreElements()) {
334 let dl = dls.getNext().QueryInterface(Ci.nsIDownload);
335 if (dl.percentComplete < 100 && dl.size > 0) {
336 mean += dl.amountTransferred;
337 base += dl.size;
338 }
339 }
340
341 // Calculate the percent transferred, unless we don't have a total file size
342 let title = gStr.downloadsTitlePercent;
343 if (base == 0)
344 title = gStr.downloadsTitleFiles;
345 else
346 mean = Math.floor((mean / base) * 100);
347
348 // Update title of window
349 if (mean != gLastComputedMean || gLastActiveDownloads != numActiveDownloads) {
350 gLastComputedMean = mean;
351 gLastActiveDownloads = numActiveDownloads;
352
353 // Get the correct plural form and insert number of downloads and percent
354 title = PluralForm.get(numActiveDownloads, title);
355 title = replaceInsert(title, 1, numActiveDownloads);
356 title = replaceInsert(title, 2, mean);
357
358 document.title = title;
359 }
360 }
361
362 ////////////////////////////////////////////////////////////////////////////////
363 //// Startup, Shutdown
364
Startup
365 function Startup()
366 {
367 gDownloadsView = document.getElementById("downloadView");
368 gSearchBox = document.getElementById("searchbox");
369
370 // convert strings to those in the string bundle
371 let (sb = document.getElementById("downloadStrings")) {
anon:372:17
372 let getStr = function(string) sb.getString(string);
373 for (let [name, value] in Iterator(gStr))
374 gStr[name] = typeof value == "string" ? getStr(value) : value.map(getStr);
375 }
376
377 buildDownloadList(true);
378
379 // The DownloadProgressListener (DownloadProgressListener.js) handles progress
380 // notifications.
381 gDownloadListener = new DownloadProgressListener();
382 gDownloadManager.addListener(gDownloadListener);
383
384 // If the UI was displayed because the user interacted, we need to make sure
385 // we update gUserInteracted accordingly.
386 if (window.arguments[1] == Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED)
387 gUserInteracted = true;
388
389 // downloads can finish before Startup() does, so check if the window should
390 // close and act accordingly
391 if (!autoRemoveAndClose())
392 gDownloadsView.focus();
393
394 let obs = Cc["@mozilla.org/observer-service;1"].
395 getService(Ci.nsIObserverService);
396 obs.addObserver(gDownloadObserver, "download-manager-remove-download", false);
397
398 // Clear the search box and move focus to the list on escape from the box
anon:399:42
399 gSearchBox.addEventListener("keypress", function(e) {
400 if (e.keyCode == e.DOM_VK_ESCAPE) {
401 // Clear the input as if the user did it
402 gSearchBox.value = "";
403 gSearchBox.doCommand();
404
405 // Move focus to the list instead of closing the window
406 gDownloadsView.focus();
407 e.preventDefault();
408 }
409 }, true);
410 }
411
Shutdown
412 function Shutdown()
413 {
414 gDownloadManager.removeListener(gDownloadListener);
415
416 let obs = Cc["@mozilla.org/observer-service;1"].
417 getService(Ci.nsIObserverService);
418 obs.removeObserver(gDownloadObserver, "download-manager-remove-download");
419
420 clearTimeout(gBuilder);
421 gStmt.reset();
422 gStmt.finalize();
423 }
424
425 let gDownloadObserver = {
gdo_observe
426 observe: function gdo_observe(aSubject, aTopic, aData) {
427 switch (aTopic) {
428 case "download-manager-remove-download":
429 // A null subject here indicates "remove all"
430 if (!aSubject) {
431 // Rebuild the default view
432 buildDownloadList(true);
433 break;
434 }
435
436 // Otherwise, remove a single download
437 let id = aSubject.QueryInterface(Ci.nsISupportsPRUint32);
438 let dl = getDownload(id.data);
439 removeFromView(dl);
440 break;
441 }
442 }
443 };
444
445 ////////////////////////////////////////////////////////////////////////////////
446 //// View Context Menus
447
448 var gContextMenus = [
449 // DOWNLOAD_DOWNLOADING
450 [
451 "menuitem_pause"
452 , "menuitem_cancel"
453 , "menuseparator"
454 , "menuitem_show"
455 , "menuseparator"
456 , "menuitem_openReferrer"
457 , "menuitem_copyLocation"
458 , "menuseparator"
459 , "menuitem_selectAll"
460 , "menuseparator"
461 , "menuitem_clearList"
462 ],
463 // DOWNLOAD_FINISHED
464 [
465 "menuitem_open"
466 , "menuitem_show"
467 , "menuseparator"
468 , "menuitem_openReferrer"
469 , "menuitem_copyLocation"
470 , "menuseparator"
471 , "menuitem_selectAll"
472 , "menuseparator"
473 , "menuitem_removeFromList"
474 , "menuitem_clearList"
475 ],
476 // DOWNLOAD_FAILED
477 [
478 "menuitem_retry"
479 , "menuseparator"
480 , "menuitem_openReferrer"
481 , "menuitem_copyLocation"
482 , "menuseparator"
483 , "menuitem_selectAll"
484 , "menuseparator"
485 , "menuitem_removeFromList"
486 , "menuitem_clearList"
487 ],
488 // DOWNLOAD_CANCELED
489 [
490 "menuitem_retry"
491 , "menuseparator"
492 , "menuitem_openReferrer"
493 , "menuitem_copyLocation"
494 , "menuseparator"
495 , "menuitem_selectAll"
496 , "menuseparator"
497 , "menuitem_removeFromList"
498 , "menuitem_clearList"
499 ],
500 // DOWNLOAD_PAUSED
501 [
502 "menuitem_resume"
503 , "menuitem_cancel"
504 , "menuseparator"
505 , "menuitem_show"
506 , "menuseparator"
507 , "menuitem_openReferrer"
508 , "menuitem_copyLocation"
509 , "menuseparator"
510 , "menuitem_selectAll"
511 , "menuseparator"
512 , "menuitem_clearList"
513 ],
514 // DOWNLOAD_QUEUED
515 [
516 "menuitem_cancel"
517 , "menuseparator"
518 , "menuitem_show"
519 , "menuseparator"
520 , "menuitem_openReferrer"
521 , "menuitem_copyLocation"
522 , "menuseparator"
523 , "menuitem_selectAll"
524 , "menuseparator"
525 , "menuitem_clearList"
526 ],
527 // DOWNLOAD_BLOCKED_PARENTAL
528 [
529 "menuitem_openReferrer"
530 , "menuitem_copyLocation"
531 , "menuseparator"
532 , "menuitem_selectAll"
533 , "menuseparator"
534 , "menuitem_removeFromList"
535 , "menuitem_clearList"
536 ],
537 // DOWNLOAD_SCANNING
538 [
539 "menuitem_show"
540 , "menuseparator"
541 , "menuitem_openReferrer"
542 , "menuitem_copyLocation"
543 , "menuseparator"
544 , "menuitem_selectAll"
545 , "menuseparator"
546 , "menuitem_clearList"
547 ],
548 // DOWNLOAD_DIRTY
549 [
550 "menuitem_openReferrer"
551 , "menuitem_copyLocation"
552 , "menuseparator"
553 , "menuitem_selectAll"
554 , "menuseparator"
555 , "menuitem_removeFromList"
556 , "menuitem_clearList"
557 ],
558 // DOWNLOAD_BLOCKED_POLICY
559 [
560 "menuitem_openReferrer"
561 , "menuitem_copyLocation"
562 , "menuseparator"
563 , "menuitem_selectAll"
564 , "menuseparator"
565 , "menuitem_removeFromList"
566 , "menuitem_clearList"
567 ]
568 ];
569
570 function buildContextMenu(aEvent)
571 {
572 if (aEvent.target.id != "downloadContextMenu")
573 return false;
574
575 var popup = document.getElementById("downloadContextMenu");
576 while (popup.hasChildNodes())
577 popup.removeChild(popup.firstChild);
578
579 if (gDownloadsView.selectedItem) {
580 let dl = gDownloadsView.selectedItem;
581 let idx = parseInt(dl.getAttribute("state"));
582 if (idx < 0)
583 idx = 0;
584
585 var menus = gContextMenus[idx];
586 for (let i = 0; i < menus.length; ++i) {
587 let menuitem = document.getElementById(menus[i]).cloneNode(true);
588 let cmd = menuitem.getAttribute("cmd");
589 if (cmd)
590 menuitem.disabled = !gDownloadViewController.isCommandEnabled(cmd, dl);
591
592 popup.appendChild(menuitem);
593 }
594
595 return true;
596 }
597
598 return false;
599 }
600
601 ////////////////////////////////////////////////////////////////////////////////
602 //// Drag and Drop
603
604 var gDownloadDNDObserver =
605 {
onDragOver
606 onDragOver: function (aEvent, aFlavour, aDragSession)
607 {
608 aDragSession.canDrop = true;
609 },
610
onDrop
611 onDrop: function(aEvent, aXferData, aDragSession)
612 {
613 var split = aXferData.data.split("\n");
614 var url = split[0];
615 if (url != aXferData.data) { //do nothing, not a valid URL
616 var name = split[1];
617 saveURL(url, name, null, true, true);
618 }
619 },
620 _flavourSet: null,
getSupportedFlavours
621 getSupportedFlavours: function ()
622 {
623 if (!this._flavourSet) {
624 this._flavourSet = new FlavourSet();
625 this._flavourSet.appendFlavour("text/x-moz-url");
626 this._flavourSet.appendFlavour("text/unicode");
627 }
628 return this._flavourSet;
629 }
630 }
631
632 ////////////////////////////////////////////////////////////////////////////////
633 //// Command Updating and Command Handlers
634
635 var gDownloadViewController = {
isCommandEnabled
636 isCommandEnabled: function(aCommand, aItem)
637 {
638 // This switch statement is for commands that do not need a download object
639 switch (aCommand) {
640 case "cmd_clearList":
641 return gDownloadManager.canCleanUp;
642 }
643
644 let dl = aItem;
645 let download = null; // used for getting an nsIDownload object
646
647 switch (aCommand) {
648 case "cmd_cancel":
649 return dl.inProgress;
650 case "cmd_open": {
651 let file = getLocalFileFromNativePathOrUrl(dl.getAttribute("file"));
652 return dl.openable && file.exists();
653 }
654 case "cmd_show": {
655 let file = getLocalFileFromNativePathOrUrl(dl.getAttribute("file"));
656 return file.exists();
657 }
658 case "cmd_pause":
659 download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
660 return dl.inProgress && !dl.paused && download.resumable;
661 case "cmd_pauseResume":
662 download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
663 return (dl.inProgress || dl.paused) && download.resumable;
664 case "cmd_resume":
665 download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
666 return dl.paused && download.resumable;
667 case "cmd_openReferrer":
668 return dl.hasAttribute("referrer");
669 case "cmd_removeFromList":
670 case "cmd_retry":
671 return dl.removable;
672 case "cmd_copyLocation":
673 return true;
674 }
675 return false;
676 },
677
doCommand
678 doCommand: function(aCommand, aItem)
679 {
680 if (this.isCommandEnabled(aCommand, aItem))
681 this.commands[aCommand](aItem);
682 },
683
684 commands: {
cmd_cancel
685 cmd_cancel: function(aSelectedItem) {
686 cancelDownload(aSelectedItem);
687 },
cmd_open
688 cmd_open: function(aSelectedItem) {
689 openDownload(aSelectedItem);
690 },
cmd_openReferrer
691 cmd_openReferrer: function(aSelectedItem) {
692 openReferrer(aSelectedItem);
693 },
cmd_pause
694 cmd_pause: function(aSelectedItem) {
695 pauseDownload(aSelectedItem);
696 },
cmd_pauseResume
697 cmd_pauseResume: function(aSelectedItem) {
698 if (aSelectedItem.paused)
699 this.cmd_resume(aSelectedItem);
700 else
701 this.cmd_pause(aSelectedItem);
702 },
cmd_removeFromList
703 cmd_removeFromList: function(aSelectedItem) {
704 removeDownload(aSelectedItem);
705 },
cmd_resume
706 cmd_resume: function(aSelectedItem) {
707 resumeDownload(aSelectedItem);
708 },
cmd_retry
709 cmd_retry: function(aSelectedItem) {
710 retryDownload(aSelectedItem);
711 },
cmd_show
712 cmd_show: function(aSelectedItem) {
713 showDownload(aSelectedItem);
714 },
cmd_copyLocation
715 cmd_copyLocation: function(aSelectedItem) {
716 copySourceLocation(aSelectedItem);
717 },
cmd_clearList
718 cmd_clearList: function() {
719 // If we're performing all, we can save some work by only doing it once
720 if (gPerformAllCallback === null)
anon:721:30
721 gPerformAllCallback = function() {};
722 else if (gPerformAllCallback)
723 return;
724
725 // Clear the whole list if there's no search
726 if (gSearchTerms == "") {
727 gDownloadManager.cleanUp();
728 }
729 else {
730 // Remove each download starting from the end until we hit a download
731 // that is in progress
732 let item;
733 while ((item = gDownloadsView.lastChild) && !item.inProgress)
734 removeDownload(item);
735
736 // Clear the input as if the user did it and move focus to the list
737 gSearchBox.value = "";
738 gSearchBox.doCommand();
739 gDownloadsView.focus();
740 }
741 }
742 }
743 };
744
745 /**
746 * Helper function to do commands.
747 *
748 * @param aCmd
749 * The command to be performed.
750 * @param aItem
751 * The richlistitem that represents the download that will have the
752 * command performed on it. If this is null, the command is performed on
753 * all downloads. If the item passed in is not a richlistitem that
754 * represents a download, it will walk up the parent nodes until it finds
755 * a DOM node that is.
756 */
performCommand
757 function performCommand(aCmd, aItem)
758 {
759 let elm = aItem;
760 if (!elm) {
761 // If we don't have a desired download item, do the command for all
762 // selected items. Initialize the callback to null so commands know to add
763 // a callback if they want. We will call the callback with empty arguments
764 // after performing the command on each selected download item.
765 gPerformAllCallback = null;
766
767 // Convert the nodelist into an array to keep a copy of the download items
768 let items = [];
769 for (let i = gDownloadsView.selectedItems.length; --i >= 0; )
770 items.unshift(gDownloadsView.selectedItems[i]);
771
772 // Do the command for each download item
773 for each (let item in items)
774 performCommand(aCmd, item);
775
776 // Call the callback with no arguments and reset because we're done
777 if (typeof gPerformAllCallback == "function")
778 gPerformAllCallback();
779 gPerformAllCallback = undefined;
780
781 return;
782 } else {
783 while (elm.nodeName != "richlistitem" ||
784 elm.getAttribute("type") != "download")
785 elm = elm.parentNode;
786 }
787
788 gDownloadViewController.doCommand(aCmd, elm);
789 }
790
setSearchboxFocus
791 function setSearchboxFocus()
792 {
793 gSearchBox.focus();
794 gSearchBox.select();
795 }
796
openExternal
797 function openExternal(aFile)
798 {
799 var uri = Cc["@mozilla.org/network/io-service;1"].
800 getService(Ci.nsIIOService).newFileURI(aFile);
801
802 var protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
803 getService(Ci.nsIExternalProtocolService);
804 protocolSvc.loadUrl(uri);
805
806 return;
807 }
808
809 ////////////////////////////////////////////////////////////////////////////////
810 //// Utility Functions
811
812 /**
813 * Create a download richlistitem with the provided attributes. Some attributes
814 * are *required* while optional ones will only be set on the item if provided.
815 *
816 * @param aAttrs
817 * An object that must have the following properties: dlid, file,
818 * target, uri, state, progress, startTime, endTime, currBytes,
819 * maxBytes; optional properties: referrer
820 * @return An initialized download richlistitem
821 */
createDownloadItem
822 function createDownloadItem(aAttrs)
823 {
824 let dl = document.createElement("richlistitem");
825
826 // Copy the attributes from the argument into the item
827 for (let attr in aAttrs)
828 dl.setAttribute(attr, aAttrs[attr]);
829
830 // Initialize other attributes
831 dl.setAttribute("type", "download");
832 dl.setAttribute("id", "dl" + aAttrs.dlid);
833 dl.setAttribute("image", "moz-icon://" + aAttrs.file + "?size=32");
834 dl.setAttribute("lastSeconds", Infinity);
835
836 // Initialize more complex attributes
837 updateTime(dl);
838 updateStatus(dl);
839
840 try {
841 let file = getLocalFileFromNativePathOrUrl(aAttrs.file);
842 dl.setAttribute("path", file.nativePath || file.path);
843 return dl;
844 } catch (e) {
845 // aFile might not be a file: url or a valid native path
846 // see bug #392386 for details
847 }
848 return null;
849 }
850
851 /**
852 * Updates the disabled state of the buttons of a downlaod.
853 *
854 * @param aItem
855 * The richlistitem representing the download.
856 */
updateButtons
857 function updateButtons(aItem)
858 {
859 let buttons = aItem.buttons;
860
861 for (let i = 0; i < buttons.length; ++i) {
862 let cmd = buttons[i].getAttribute("cmd");
863 let enabled = gDownloadViewController.isCommandEnabled(cmd, aItem);
864 buttons[i].disabled = !enabled;
865
866 if ("cmd_pause" == cmd && !enabled) {
867 // We need to add the tooltip indicating that the download cannot be
868 // paused now.
869 buttons[i].setAttribute("tooltiptext", gStr.cannotPause);
870 }
871 }
872 }
873
874 /**
875 * Updates the status for a download item depending on its state
876 *
877 * @param aItem
878 * The richlistitem that has various download attributes.
879 * @param aDownload
880 * The nsDownload from the backend. This is an optional parameter, but
881 * is useful for certain states such as DOWNLOADING.
882 */
updateStatus
883 function updateStatus(aItem, aDownload) {
884 let status = "";
885 let statusTip = "";
886
887 let state = Number(aItem.getAttribute("state"));
888 switch (state) {
889 case nsIDM.DOWNLOAD_PAUSED:
890 {
891 let currBytes = Number(aItem.getAttribute("currBytes"));
892 let maxBytes = Number(aItem.getAttribute("maxBytes"));
893
894 let transfer = DownloadUtils.getTransferTotal(currBytes, maxBytes);
895 status = replaceInsert(gStr.paused, 1, transfer);
896
897 break;
898 }
899 case nsIDM.DOWNLOAD_DOWNLOADING:
900 {
901 let currBytes = Number(aItem.getAttribute("currBytes"));
902 let maxBytes = Number(aItem.getAttribute("maxBytes"));
903 // If we don't have an active download, assume 0 bytes/sec
904 let speed = aDownload ? aDownload.speed : 0;
905 let lastSec = Number(aItem.getAttribute("lastSeconds"));
906
907 let newLast;
908 [status, newLast] =
909 DownloadUtils.getDownloadStatus(currBytes, maxBytes, speed, lastSec);
910
911 // Update lastSeconds to be the new value
912 aItem.setAttribute("lastSeconds", newLast);
913
914 break;
915 }
916 case nsIDM.DOWNLOAD_FINISHED:
917 case nsIDM.DOWNLOAD_FAILED:
918 case nsIDM.DOWNLOAD_CANCELED:
919 case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
920 case nsIDM.DOWNLOAD_BLOCKED_POLICY:
921 case nsIDM.DOWNLOAD_DIRTY:
922 {
923 let (stateSize = {}) {
anon:924:45
924 stateSize[nsIDM.DOWNLOAD_FINISHED] = function() {
925 // Display the file size, but show "Unknown" for negative sizes
926 let fileSize = Number(aItem.getAttribute("maxBytes"));
927 let sizeText = gStr.doneSizeUnknown;
928 if (fileSize >= 0) {
929 let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
930 sizeText = replaceInsert(gStr.doneSize, 1, size);
931 sizeText = replaceInsert(sizeText, 2, unit);
932 }
933 return sizeText;
934 };
anon:935:43
935 stateSize[nsIDM.DOWNLOAD_FAILED] = function() gStr.stateFailed;
anon:936:45
936 stateSize[nsIDM.DOWNLOAD_CANCELED] = function() gStr.stateCanceled;
anon:937:53
937 stateSize[nsIDM.DOWNLOAD_BLOCKED_PARENTAL] = function() gStr.stateBlockedParentalControls;
anon:938:51
938 stateSize[nsIDM.DOWNLOAD_BLOCKED_POLICY] = function() gStr.stateBlockedPolicy;
anon:939:42
939 stateSize[nsIDM.DOWNLOAD_DIRTY] = function() gStr.stateDirty;
940
941 // Insert 1 is the download size or download state
942 status = replaceInsert(gStr.doneStatus, 1, stateSize[state]());
943 }
944
945 let [displayHost, fullHost] =
946 DownloadUtils.getURIHost(getReferrerOrSource(aItem));
947 // Insert 2 is the eTLD + 1 or other variations of the host
948 status = replaceInsert(status, 2, displayHost);
949 // Set the tooltip to be the full host
950 statusTip = fullHost;
951
952 break;
953 }
954 }
955
956 aItem.setAttribute("status", status);
957 aItem.setAttribute("statusTip", statusTip != "" ? statusTip : status);
958 }
959
960 /**
961 * Updates the time that gets shown for completed download items
962 *
963 * @param aItem
964 * The richlistitem representing a download in the UI
965 */
updateTime
966 function updateTime(aItem)
967 {
968 // Don't bother updating for things that aren't finished
969 if (aItem.inProgress)
970 return;
971
972 let dts = Cc["@mozilla.org/intl/scriptabledateformat;1"].
973 getService(Ci.nsIScriptableDateFormat);
974
975 // Figure out when today begins
976 let now = new Date();
977 let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
978
979 // Get the end time to display
980 let end = new Date(parseInt(aItem.getAttribute("endTime")));
981
982 // Figure out if the end time is from today, yesterday, this week, etc.
983 let dateTime;
984 if (end >= today) {
985 // Download finished after today started, show the time
986 dateTime = dts.FormatTime("", dts.timeFormatNoSeconds,
987 end.getHours(), end.getMinutes(), 0);
988 } else if (today - end < (24 * 60 * 60 * 1000)) {
989 // Download finished after yesterday started, show yesterday
990 dateTime = gStr.yesterday;
991 } else if (today - end < (6 * 24 * 60 * 60 * 1000)) {
992 // Download finished after last week started, show day of week
993 dateTime = end.toLocaleFormat("%A");
994 } else {
995 // Download must have been from some time ago.. show month/day
996 let month = end.toLocaleFormat("%B");
997 // Remove leading 0 by converting the date string to a number
998 let date = Number(end.toLocaleFormat("%d"));
999 dateTime = replaceInsert(gStr.monthDate, 1, month);
1000 dateTime = replaceInsert(dateTime, 2, date);
1001 }
1002
1003 aItem.setAttribute("dateTime", dateTime);
1004
1005 // Set the tooltip to be the full date and time
1006 let dateTimeTip = dts.FormatDateTime("",
1007 dts.dateFormatLong,
1008 dts.timeFormatNoSeconds,
1009 end.getFullYear(),
1010 end.getMonth() + 1,
1011 end.getDate(),
1012 end.getHours(),
1013 end.getMinutes(),
1014 0);
1015
1016 aItem.setAttribute("dateTimeTip", dateTimeTip);
1017 }
1018
1019 /**
1020 * Helper function to replace a placeholder string with a real string
1021 *
1022 * @param aText
1023 * Source text containing placeholder (e.g., #1)
1024 * @param aIndex
1025 * Index number of placeholder to replace
1026 * @param aValue
1027 * New string to put in place of placeholder
1028 * @return The string with placeholder replaced with the new string
1029 */
replaceInsert
1030 function replaceInsert(aText, aIndex, aValue)
1031 {
1032 return aText.replace("#" + aIndex, aValue);
1033 }
1034
1035 /**
1036 * Perform the default action for the currently selected download item
1037 */
doDefaultForSelected
1038 function doDefaultForSelected()
1039 {
1040 // Make sure we have something selected
1041 let item = gDownloadsView.selectedItem;
1042 if (!item)
1043 return;
1044
1045 // Get the default action (first item in the menu)
1046 let state = Number(item.getAttribute("state"));
1047 let menuitem = document.getElementById(gContextMenus[state][0]);
1048
1049 // Try to do the action if the command is enabled
1050 gDownloadViewController.doCommand(menuitem.getAttribute("cmd"), item);
1051 }
1052
removeFromView
1053 function removeFromView(aDownload)
1054 {
1055 // Make sure we have an item to remove
1056 if (!aDownload) return;
1057
1058 let index = gDownloadsView.selectedIndex;
1059 gDownloadsView.removeChild(aDownload);
1060 gDownloadsView.selectedIndex = Math.min(index, gDownloadsView.itemCount - 1);
1061
1062 // Color everything after from the newly selected item
1063 stripeifyList(gDownloadsView.selectedItem);
1064 }
1065
getReferrerOrSource
1066 function getReferrerOrSource(aDownload)
1067 {
1068 // Give the referrer if we have it set
1069 if (aDownload.hasAttribute("referrer"))
1070 return aDownload.getAttribute("referrer");
1071
1072 // Otherwise, provide the source
1073 return aDownload.getAttribute("uri");
1074 }
1075
1076 /**
1077 * Initiate building the download list to have the active downloads followed by
1078 * completed ones filtered by the search term if necessary.
1079 *
1080 * @param aForceBuild
1081 * Force the list to be built even if the search terms don't change
1082 */
buildDownloadList
1083 function buildDownloadList(aForceBuild)
1084 {
1085 // Stringify the previous search
1086 let prevSearch = gSearchTerms.join(" ");
1087
1088 // Array of space-separated lower-case search terms
1089 gSearchTerms = gSearchBox.value.replace(/^\s+|\s+$/g, "").
1090 toLowerCase().split(/\s+/);
1091
1092 // Unless forced, don't rebuild the download list if the search didn't change
1093 if (!aForceBuild && gSearchTerms.join(" ") == prevSearch)
1094 return;
1095
1096 // Clear out values before using them
1097 clearTimeout(gBuilder);
1098 gStmt.reset();
1099
1100 // Clear the list before adding items by replacing with a shallow copy
1101 let (empty = gDownloadsView.cloneNode(false)) {
1102 gDownloadsView.parentNode.replaceChild(empty, gDownloadsView);
1103 gDownloadsView = empty;
1104 }
1105
1106 try {
1107 gStmt.bindInt32Parameter(0, nsIDM.DOWNLOAD_NOTSTARTED);
1108 gStmt.bindInt32Parameter(1, nsIDM.DOWNLOAD_DOWNLOADING);
1109 gStmt.bindInt32Parameter(2, nsIDM.DOWNLOAD_PAUSED);
1110 gStmt.bindInt32Parameter(3, nsIDM.DOWNLOAD_QUEUED);
1111 gStmt.bindInt32Parameter(4, nsIDM.DOWNLOAD_SCANNING);
1112 } catch (e) {
1113 // Something must have gone wrong when binding, so clear and quit
1114 gStmt.reset();
1115 return;
1116 }
1117
1118 // Take a quick break before we actually start building the list
anon:1119:24
1119 gBuilder = setTimeout(function() {
1120 // Start building the list and select the first item
1121 stepListBuilder(1);
1122 gDownloadsView.selectedIndex = 0;
1123 }, 0);
1124 }
1125
1126 /**
1127 * Incrementally build the download list by adding at most the requested number
1128 * of items if there are items to add. After doing that, it will schedule
1129 * another chunk of items specified by gListBuildDelay and gListBuildChunk.
1130 *
1131 * @param aNumItems
1132 * Number of items to add to the list before taking a break
1133 */
stepListBuilder
1134 function stepListBuilder(aNumItems) {
1135 try {
1136 // If we're done adding all items, we can quit
1137 if (!gStmt.executeStep()) {
1138 // Send a notification that we finished
1139 Cc["@mozilla.org/observer-service;1"].
1140 getService(Ci.nsIObserverService).
1141 notifyObservers(window, "download-manager-ui-done", null);
1142
1143 return;
1144 }
1145
1146 // Try to get the attribute values from the statement
1147 let attrs = {
1148 dlid: gStmt.getInt64(0),
1149 file: gStmt.getString(1),
1150 target: gStmt.getString(2),
1151 uri: gStmt.getString(3),
1152 state: gStmt.getInt32(4),
1153 startTime: Math.round(gStmt.getInt64(5) / 1000),
1154 endTime: Math.round(gStmt.getInt64(6) / 1000),
1155 currBytes: gStmt.getInt64(8),
1156 maxBytes: gStmt.getInt64(9)
1157 };
1158
1159 // Only add the referrer if it's not null
1160 let (referrer = gStmt.getString(7)) {
1161 if (referrer)
1162 attrs.referrer = referrer;
1163 }
1164
1165 // If the download is active, grab the real progress, otherwise default 100
1166 let isActive = gStmt.getInt32(10);
1167 attrs.progress = isActive ? gDownloadManager.getDownload(attrs.dlid).
1168 percentComplete : 100;
1169
1170 // Make the item and add it to the end if it's active or matches the search
1171 let item = createDownloadItem(attrs);
1172 if (item && (isActive || downloadMatchesSearch(item))) {
1173 // Add item to the end and color just that one item
1174 gDownloadsView.appendChild(item);
1175 stripeifyList(item);
1176
1177 // Because of the joys of XBL, we can't update the buttons until the
1178 // download object is in the document.
1179 updateButtons(item);
1180 } else {
1181 // We didn't add an item, so bump up the number of items to process, but
1182 // not a whole number so that we eventually do pause for a chunk break
1183 aNumItems += .9;
1184 }
1185 } catch (e) {
1186 // Something went wrong when stepping or getting values, so clear and quit
1187 gStmt.reset();
1188 return;
1189 }
1190
1191 // Add another item to the list if we should; otherwise, let the UI update
1192 // and continue later
1193 if (aNumItems > 1) {
1194 stepListBuilder(aNumItems - 1);
1195 } else {
1196 // Use a shorter delay for earlier downloads to display them faster
1197 let delay = Math.min(gDownloadsView.itemCount * 10, gListBuildDelay);
1198 gBuilder = setTimeout(stepListBuilder, delay, gListBuildChunk);
1199 }
1200 }
1201
1202 /**
1203 * Add a download to the front of the download list
1204 *
1205 * @param aDownload
1206 * The nsIDownload to make into a richlistitem
1207 */
prependList
1208 function prependList(aDownload)
1209 {
1210 let attrs = {
1211 dlid: aDownload.id,
1212 file: aDownload.target.spec,
1213 target: aDownload.displayName,
1214 uri: aDownload.source.spec,
1215 state: aDownload.state,
1216 progress: aDownload.percentComplete,
1217 startTime: Math.round(aDownload.startTime / 1000),
1218 endTime: Date.now(),
1219 currBytes: aDownload.amountTransferred,
1220 maxBytes: aDownload.size
1221 };
1222
1223 // Make the item and add it to the beginning
1224 let item = createDownloadItem(attrs);
1225 if (item) {
1226 // Add item to the beginning and color the whole list
1227 gDownloadsView.insertBefore(item, gDownloadsView.firstChild);
1228 stripeifyList(item);
1229
1230 // Because of the joys of XBL, we can't update the buttons until the
1231 // download object is in the document.
1232 updateButtons(item);
1233 }
1234 }
1235
1236 /**
1237 * Check if the download matches the current search term based on the texts
1238 * shown to the user. All search terms are checked to see if each matches any
1239 * of the displayed texts.
1240 *
1241 * @param aItem
1242 * Download richlistitem to check if it matches the current search
1243 * @return Boolean true if it matches the search; false otherwise
1244 */
downloadMatchesSearch
1245 function downloadMatchesSearch(aItem)
1246 {
1247 // Search through the download attributes that are shown to the user and
1248 // make it into one big string for easy combined searching
1249 let combinedSearch = "";
1250 for each (let attr in gSearchAttributes)
1251 combinedSearch += aItem.getAttribute(attr).toLowerCase() + " ";
1252
1253 // Make sure each of the terms are found
1254 for each (let term in gSearchTerms)
1255 if (combinedSearch.search(term) == -1)
1256 return false;
1257
1258 return true;
1259 }
1260
1261 /**
1262 * Stripeify the download list by setting or clearing the "alternate" attribute
1263 * on items starting from a particular item and continuing to the end.
1264 *
1265 * @param aItem
1266 * Download rishlist item to start stripeifying
1267 */
stripeifyList
1268 function stripeifyList(aItem)
1269 {
1270 let alt = "alternate";
1271 // Set the item to be opposite of the other
anon:1272:17
1272 let flipFrom = function(aOther) aOther && aOther.hasAttribute(alt) ?
1273 aItem.removeAttribute(alt) : aItem.setAttribute(alt, "true");
1274
1275 // Keep coloring items as the opposite of its previous until no more
1276 while (aItem) {
1277 flipFrom(aItem.previousSibling);
1278 aItem = aItem.nextSibling;
1279 }
1280 }
1281
1282 // we should be using real URLs all the time, but until
1283 // bug 239948 is fully fixed, this will do...
1284 //
1285 // note, this will thrown an exception if the native path
1286 // is not valid (for example a native Windows path on a Mac)
1287 // see bug #392386 for details
getLocalFileFromNativePathOrUrl
1288 function getLocalFileFromNativePathOrUrl(aPathOrUrl)
1289 {
1290 if (aPathOrUrl.substring(0,7) == "file://") {
1291 // if this is a URL, get the file from that
1292 let ioSvc = Cc["@mozilla.org/network/io-service;1"].
1293 getService(Ci.nsIIOService);
1294
1295 // XXX it's possible that using a null char-set here is bad
1296 const fileUrl = ioSvc.newURI(aPathOrUrl, null, null).
1297 QueryInterface(Ci.nsIFileURL);
1298 return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
1299 } else {
1300 // if it's a pathname, create the nsILocalFile directly
1301 var f = new nsLocalFile(aPathOrUrl);
1302
1303 return f;
1304 }
1305 }