toolkit/mozapps/downloads/content/downloads.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

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;
  1009   aItem.setAttribute("status", status);
  1010   aItem.setAttribute("statusTip", statusTip != "" ? statusTip : status);
  1013 /**
  1014  * Updates the time that gets shown for completed download items
  1016  * @param aItem
  1017  *        The richlistitem representing a download in the UI
  1018  */
  1019 function updateTime(aItem)
  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);
  1031 /**
  1032  * Helper function to replace a placeholder string with a real string
  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)
  1044   return aText.replace("#" + aIndex, aValue);
  1047 /**
  1048  * Perform the default action for the currently selected download item
  1049  */
  1050 function doDefaultForSelected()
  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);
  1065 function removeFromView(aDownload)
  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();
  1078 function getReferrerOrSource(aDownload)
  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");
  1088 /**
  1089  * Initiate building the download list to have the active downloads followed by
  1090  * completed ones filtered by the search term if necessary.
  1092  * @param aForceBuild
  1093  *        Force the list to be built even if the search terms don't change
  1094  */
  1095 function buildDownloadList(aForceBuild)
  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;
  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;
  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);
  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.
  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;
  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;
  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;
  1199   } catch (e) {
  1200     // Something went wrong when stepping or getting values, so clear and quit
  1201     gStmt.reset();
  1202     return;
  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);
  1216 /**
  1217  * Add a download to the front of the download list
  1219  * @param aDownload
  1220  *        The nsIDownload to make into a richlistitem
  1221  */
  1222 function prependList(aDownload)
  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();
  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.
  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)
  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;
  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)
  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;
  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()
  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);
  1313 function getDownload(aID)
  1315   return document.getElementById("dl" + aID);
  1318 /**
  1319  * Initialize the statement which is used to retrieve the list of downloads.
  1320  */
  1321 function initStatement()
  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");

mercurial