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