root/trunk/chrome/content/dta/manager/manager.js

Revision 1741, 81.7 kB (checked in by MaierMan, 7 months ago)

move manager.js

Line 
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
4  * The contents of this file are subject to the Mozilla Public License Version
5  * 1.1 (the "License"); you may not use this file except in compliance with
6  * the License. You may obtain a copy of the License at
7  * http://www.mozilla.org/MPL/
8  *
9  * Software distributed under the License is distributed on an "AS IS" basis,
10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11  * for the specific language governing rights and limitations under the
12  * License.
13  *
14  * The Original Code is DownThemAll!
15  *
16  * The Initial Developers of the Original Code are Stefano Verna and Federico Parodi
17  * Portions created by the Initial Developers are Copyright (C) 2004-2007
18  * the Initial Developers. All Rights Reserved.
19  *
20  * Contributor(s):
21  *    Stefano Verna <stefano.verna@gmail.com>
22  *    Federico Parodi <jimmy2k@gmail.com>
23  *    Nils Maier <MaierMan@web.de>
24  *
25  * Alternatively, the contents of this file may be used under the terms of
26  * either the GNU General Public License Version 2 or later (the "GPL"), or
27  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28  * in which case the provisions of the GPL or the LGPL are applicable instead
29  * of those above. If you wish to allow use of your version of this file only
30  * under the terms of either the GPL or the LGPL, and not to allow others to
31  * use your version of this file under the terms of the MPL, indicate your
32  * decision by deleting the provisions above and replace them with the notice
33  * and other provisions required by the GPL or the LGPL. If you do not delete
34  * the provisions above, a recipient may use your version of this file under
35  * the terms of any one of the MPL, the GPL or the LGPL.
36  *
37  * ***** END LICENSE BLOCK ***** */
38  
39 const NS_DTA = 'http://www.downthemall.net/properties#';
40 const NS_METALINKER = 'http://www.metalinker.org/';
41 const NS_HTML = 'http://www.w3.org/1999/xhtml';
42  
43  
44 const NS_ERROR_MODULE_NETWORK = 0x804B0000;
45 const NS_ERROR_BINDING_ABORTED = NS_ERROR_MODULE_NETWORK + 2;
46 const NS_ERROR_UNKNOWN_HOST = NS_ERROR_MODULE_NETWORK + 30;
47 const NS_ERROR_CONNECTION_REFUSED = NS_ERROR_MODULE_NETWORK + 13;
48 const NS_ERROR_NET_TIMEOUT = NS_ERROR_MODULE_NETWORK + 14;
49 const NS_ERROR_NET_RESET = NS_ERROR_MODULE_NETWORK + 20;
50 const NS_ERROR_FTP_CWD = NS_ERROR_MODULE_NETWORK + 22;
51
52 const Construct = Components.Constructor;
53 function Serv(c, i) {
54   return Cc[c].getService(i ? Ci[i] : null);
55 }
56
57 const BufferedOutputStream = Construct('@mozilla.org/network/buffered-output-stream;1', 'nsIBufferedOutputStream', 'init');
58 const FileInputStream = Construct('@mozilla.org/network/file-input-stream;1', 'nsIFileInputStream', 'init');
59 const FileOutputStream = Construct('@mozilla.org/network/file-output-stream;1', 'nsIFileOutputStream', 'init');
60 const StringInputStream = Construct('@mozilla.org/io/string-input-stream;1', 'nsIStringInputStream', 'setData');
61
62 ServiceGetter(this, "ContentHandling", "@downthemall.net/contenthandling;2", "dtaIContentHandling");
63 ServiceGetter(this, "MimeService", "@mozilla.org/uriloader/external-helper-app-service;1", "nsIMIMEService");
64 ServiceGetter(this, "ObserverService", "@mozilla.org/observer-service;1", "nsIObserverService");
65 ServiceGetter(this, "WindowWatcherService", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher");
66
67 const MIN_CHUNK_SIZE = 512 * 1024;
68
69 // amount to buffer in BufferedOutputStream
70 // furthermore up to this ammount will automagically discared after crashes
71 const CHUNK_BUFFER_SIZE = 96 * 1024;
72
73 const REFRESH_FREQ = 1000;
74 const STREAMS_FREQ = 250;
75
76 let Prompts = {}, Preallocator = {}, Limits = {}, JSONCompat = {}, PrivateBrowsing = {};
77 module('resource://dta/prompts.jsm', Prompts);
78 module('resource://dta/speedstats.jsm');
79 module('resource://dta/preallocator.jsm', Preallocator);
80 module('resource://dta/cothread.jsm');
81 module('resource://dta/queuestore.jsm');
82 module('resource://dta/timers.jsm');
83 module('resource://dta/loggedprompter.jsm');
84 module('resource://dta/serverlimits.jsm', Limits);
85 module('resource://dta/json.jsm', JSONCompat);
86 module('resource://dta/urlmanager.jsm');
87 module('resource://dta/visitormanager.jsm');
88 module('resource://dta/decompressor.jsm');
89 module('resource://dta/verificator.jsm');
90 module('resource://dta/bytebucket.jsm');
91 module('resource://dta/pbm.jsm', PrivateBrowsing);
92
93 const AuthPrompts = new LoggedPrompter(window);
94
95 var TEXT_PAUSED;
96 var TEXT_QUEUED;
97 var TEXT_COMPLETE;
98 var TEXT_CANCELED;
99
100
101 var GlobalBucket = null;
102 var Timers = new TimerManager();
103
104 const Dialog = {
105   _observes: [
106     'quit-application-requested',
107     'quit-application-granted',
108     'network:offline-status-changed'
109   ],
110   _initialized: false,
111   _autoRetrying: [],
112   _offline: false,
113   _maxObservedSpeed: 0,
114   _infoWindows: [],
115   get offline() {
116     return this._offline || this._offlineForced;
117   },
118   set offline(nv) {
119     this._offline = !!nv;
120     $('cmdToggleOffline').setAttribute('disabled', this._offline);
121     this._processOfflineChange();
122     return this._offline;
123   },
124   get offlineForced() {
125     return this._offlineForced;
126   },
127   set offlineForced(nv) {
128     this._offlineForced = !!nv;
129     let netstatus = $('netstatus');
130     if (this._offlineForced) {
131       netstatus.setAttribute('offline', true);
132     }
133     else if (netstatus.hasAttribute('offline')) {
134       netstatus.removeAttribute('offline');
135     }   
136     this._processOfflineChange();
137     return this._offlineForced;
138   },
139  
140   _wasRunning: false,
141   _sum: 0,
142   _speeds: new SpeedStats(10),
143   _running: [],
144   _autoClears: [],
145   completed: 0,
146   totalbytes: 0,
147   init: function D_init() {
148     removeEventListener('load', arguments.callee, false);
149    
150     TEXT_PAUSED = _('paused');
151     TEXT_QUEUED = _('queued');
152     TEXT_COMPLETE = _('complete');
153     TEXT_CANCELED = _('canceled');
154
155     (function initListeners() {
156       addEventListener('unload', function() Dialog.unload(), false);
157       addEventListener('close', function(evt) {
158         let rv = Dialog.close();
159         if (!rv) {
160           evt.preventDefault();
161         }
162         return rv;
163       }, true);
164       addEventListener('dragover', function(event) nsDragAndDrop.dragOver(event, DTA_DropDTA), true);
165       addEventListener('drop', function(event) nsDragAndDrop.drop(event, DTA_DropDTA), true);
166      
167       $('tooldonate').addEventListener('click', function() Dialog.openDonate(), false);
168     })();   
169    
170     Tree.init($("downloads"));
171     try {
172       Timers.createOneshot(100, this._loadDownloads, this);
173     }
174     catch (ex) {
175       Debug.log("Failed to load any downloads from queuefile", ex);
176     }
177
178     try {
179       this.offline = IOService.offline;
180     }
181     catch (ex) {
182       Debug.log("Cannot get offline status", ex);
183     }
184    
185     Preferences.makeObserver(this);
186     this._observes.forEach(
187       function(topic) {
188         ObserverService.addObserver(this, topic, true);
189       },
190       this
191     );
192      
193     (function autofit() {
194       let de = document.documentElement;
195       let version = {};
196       Components.utils.import('resource://dta/version.jsm', version);
197       let cv = version.VERSION + ".toolitems" + $('tools').childNodes.length;
198       let shouldAutofit = !de.hasAttribute('dtaAutofitted');
199       if (!shouldAutofit) {
200         try {
201           let lv = de.getAttribute('dtaAutofitted');
202           shouldAutofit = !!version.compareVersion(cv, lv);
203         }
204         catch (ex) {
205           shouldAutofit = true;
206         }
207       }
208       if (shouldAutofit) {
209         document.documentElement.setAttribute('dtaAutofitted', cv);
210         setTimeout(
211           function() {
212             let tdb = $('tooldonate').boxObject;
213             let db = de.boxObject
214             let cw = tdb.width + tdb.x;
215             if (db.width < cw) {
216               window.resizeTo(cw, window.outerHeight);
217               Debug.logString("manager was autofit");
218             }
219           },
220           10
221         );
222       }
223     })();
224     (function() {
225       for each (let e in Array.map(document.getElementsByTagName('toolbarbutton'), function(e) e)) {
226         if (!e.hasAttribute('tooltiptext')) {
227           e.setAttribute('tooltiptext', e.getAttribute('label'));
228         }
229       }
230      
231       $('tbp_' + $('tools').getAttribute('mode')).setAttribute('checked', "true");
232     })();
233     GlobalBucket = new ByteBucket(Prefs.speedLimit, 1.3);
234     $('listSpeeds').limit = Prefs.speedLimit;
235    
236     (function nagging() {
237       if (Preferences.getExt('nagnever', false)) {
238         return;
239       }
240       let nb = $('notifications');
241       try {
242         let seq = QueueStore.getQueueSeq();
243         let nagnext = Preferences.getExt('nagnext', 100);
244         Debug.logString("nag: " + seq + "/" + nagnext + "/" + (seq - nagnext));
245         if (seq < nagnext) {
246           return;
247         }
248         for (nagnext = isFinite(nagnext) && nagnext > 0 ? nagnext : 100; seq >= nagnext; nagnext *= 2);
249        
250         seq = Math.floor(seq / 100) * 100;
251
252         setTimeout(function() {
253           let ndonation = nb.appendNotification(
254               _('nagtext', [seq]),
255               "donation",
256               null,
257               nb.PRIORITY_INFO_HIGH,
258               [
259                 {
260                   accessKey: '',
261                   label: _('nagdonate'),
262                   callback: function() {
263                     nb.removeNotification(ndonation);
264                     Preferences.setExt('nagnext', nagnext);
265                     Dialog.openDonate();
266                   }
267                 },
268                 {
269                   accessKey: '',
270                   label: _('naghide'),
271                   callback: function() {
272                     Preferences.setExt('nagnext', nagnext);
273                     nb.removeNotification(ndonation);
274                   }
275                 },
276                 {
277                   accessKey: '',
278                   label: _('nagneveragain'),
279                   callback: function() {
280                     nb.removeNotification(ndonation);
281                     Preferences.setExt('nagnever', true);
282                   }
283                 }
284
285               ]
286           )
287         }, 1000);
288       }
289       catch (ex) {
290         Debug.log('nagger', ex);
291       }
292     })();   
293   },
294  
295   customizeToolbar: function(evt) {
296     $('tools').setAttribute('mode', evt.target.getAttribute('mode'));
297   },
298  
299   changeSpeedLimit: function() {
300     let list = $('listSpeeds');
301     let val = list.limit;
302     Preferences.setExt('speedlimit', val);
303     GlobalBucket.byteRate = val;
304     this._speeds.clear();
305   },
306  
307   _loadDownloads: function D__loadDownloads() {
308     this._loading = $('loading');
309     if (!this._loading) {
310       this._loading = {};
311     }
312     Tree.beginUpdate();
313     Tree.clear();
314     this._brokenDownloads = [];
315     Debug.logString("loading of the queue started!");
316     this._loader = new CoThreadListWalker(
317       this._loadDownloads_item,
318       QueueStore.loadGenerator(),
319       250,
320       this,
321       this._loadDownloads_finish
322     );
323     this._loader.run();   
324   },
325   _loadDownloads_item: function D__loadDownloads_item(dbItem, idx) {
326     if (idx % 500 == 0) {
327       this._loading.label = _('loading', [idx, dbItem.count, Math.floor(idx * 100 / dbItem.count)]);
328     }
329    
330     try {
331       let down = JSONCompat.parse(dbItem.serial);
332      
333       let get = function(attr, def) {
334         return (attr in down) ? down[attr] : (def ? def : '');
335       }
336
337       let d = new QueueItem();
338       d.dbId = dbItem.id;
339       let state = get('state');
340       if (state) {
341         d._state = state;
342       }         
343       d.urlManager = new UrlManager(down.urlManager);
344       d.numIstance = get("numIstance");
345
346       let referrer = get('referrer');
347       if (referrer) {
348         try {
349           d.referrer = referrer.toURL();
350         }
351         catch (ex) {
352           // We might have been fed with about:blank or other crap. so ignore.
353         }
354       }
355    
356       // only access the setter of the last so that we don't generate stuff trice.
357       d._pathName = get('pathName', '');
358       d._description = get('description', '');
359       d._title = get('title', '');
360       d._mask = get('mask');
361       d.fileName = get('fileName');
362      
363       let tmpFile = get('tmpFile');
364       if (tmpFile) {
365         try {
366           tmpFile = new FileFactory(tmpFile);
367           if (tmpFile.exists()) {
368             d._tmpFile = tmpFile;
369           }
370           else {
371             // Download partfile is gone!
372             // XXX find appropriate error message!
373             d.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
374           }
375         }
376         catch (ex) {
377           Debug.log("tried to construct with invalid tmpFile", ex);
378           d.cancel();
379         }
380       }       
381
382       d.startDate = new Date(get("startDate"));
383       d.visitors = new VisitorManager(down.visitors);
384      
385       for each (let e in [
386         'contentType',
387         'conflicts',
388         'postData',
389         'destinationName',
390         'resumable',
391         'compression',
392         'fromMetalink',
393         'speedLimit',
394       ].filter(function(e) e in down)) {
395         d[e] = down[e];
396       }
397      
398       // don't trigger prealloc!
399       d._totalSize = down.totalSize ? down.totalSize : 0;
400
401       if (down.hashCollection) {
402         d.hashCollection = DTA.HashCollection.load(down.hashCollection);
403       }
404       else if (down.hash) {
405         d.hashCollection = new DTA.HashCollection(new DTA.Hash(down.hash, down.hashType));
406       }
407       if ('maxChunks' in down) {
408         d._maxChunks = down.maxChunks;
409       }
410
411       d.started = d.partialSize != 0;
412       switch (d._state) {
413         case PAUSED:
414         case QUEUED:
415         {
416           for each (let c in down.chunks) {
417             d.chunks.push(new Chunk(d, c.start, c.end, c.written));
418           }
419           d.refreshPartialSize();
420           if (d._state == PAUSED) {
421             d.status = TEXT_PAUSED;
422           }
423           else {
424             d.status = TEXT_QUEUED;
425           }
426         }
427         break;
428        
429         case COMPLETE:
430           d.partialSize = d.totalSize;
431           d.status = TEXT_COMPLETE;
432         break;
433        
434         case CANCELED:
435           d.status = TEXT_CANCELED;
436         break;
437       }
438      
439       // XXX better call this only once
440       // See above
441       d.rebuildDestination();
442
443       d._position = Tree.add(d);
444     }
445     catch (ex) {
446       Debug.log('failed to init download #' + dbItem.id + ' from queuefile', ex);
447       this._brokenDownloads.push(dbItem.id);
448     }
449     return true;
450   },
451   _loadDownloads_finish: function D__loadDownloads_finish() {
452     delete this._loader;
453     Tree.endUpdate();
454     Tree.invalidate();
455    
456     if (this._brokenDownloads.length) {
457       QueueStore.beginUpdate();
458       try {
459         for each (let id in this._brokenDownloads) {
460           QueueStore.deleteDownload(id);
461           Debug.logString("Removed broken download #" + id);
462         }
463       }
464       catch (ex) {
465         Debug.log("failed to remove broken downloads", ex);
466       }
467       QueueStore.endUpdate();
468     }
469     delete this._brokenDownloads;
470     delete this._loading;
471    
472     this._updTimer = Timers.createRepeating(REFRESH_FREQ, this.checkDownloads, this, true);   
473    
474     this.start();
475   }, 
476  
477   enterPrivateBrowsing: function() {
478     this.reinit();
479   },
480   exitPrivateBrowsing: function() {
481     Tree.updateAll(function(download) {
482       if (!download.is(COMPLETE)) {
483         download.cancel();
484       }
485       return true;
486     });
487     this.reinit();
488   },
489   canEnterPrivateBrowsing: function() {
490     if (Tree.some(function(d) { return d.started && !d.resumable && d.isOf(RUNNING); })) {
491       var rv = Prompts.confirmYN(
492         window,
493         _("confpbm"),
494         _("nonrespbm")
495       );
496       if (rv) {
497         return false;
498       }
499     }
500     return (this._forceClose = true);
501   },
502   canExitPrivateBrowsing: function() {
503     if (Tree.some(function(d) { return d.isOf(RUNNING, QUEUED, PAUSED, FINISHING); })) {
504       var rv = Prompts.confirmYN(
505         window,
506         _("confleavepbm"),
507         _("nonleavepbm")
508       );
509       if (rv) {
510         return false;
511       }
512     }
513     return (this._forceClose = true);
514   },
515  
516   openAdd: function D_openAdd() {
517     window.openDialog(
518       'chrome://dta/content/dta/addurl.xul',
519       '_blank',
520       'chrome, centerscreen, dialog=no, dependent=yes'
521     );
522   },
523  
524   openDonate: function D_openDonate() {
525     try {
526       DTA_Mediator.open('http://www.downthemall.net/howto/donate/');
527     }
528     catch(ex) {
529       alert(ex);
530     }
531   },
532   openInfo: function D_openInfo(downloads) {
533     let w = window.openDialog(
534       "chrome://dta/content/dta/manager/info.xul","_blank",
535       "chrome, centerscreen, dialog=no",
536       downloads,
537       this
538       );
539     if (w) {
540       this._infoWindows.push(w);
541     }
542   },
543  
544   start: function D_start() {
545     if (this._initialized) {
546       return;
547     }
548
549     PrivateBrowsing.registerCallbacks(this);
550    
551     if ("arguments" in window) {
552       startDownloads(window.arguments[0], window.arguments[1]);
553     }
554     this._initialized = true;
555     for (let d in Tree.all) {
556       if (d.is(FINISHING)) {
557         this.run(d);
558       }
559     }
560     Timers.createRepeating(100, this.refreshWritten, this, true);
561     Timers.createRepeating(10000, this.saveRunning, this);
562    
563     $('loadingbox').parentNode.removeChild($('loadingbox'));
564   },
565  
566   reinit: function() {
567     if (!this._initialized) {
568       return;
569     }
570     try {
571       Debug.logString("reinit initiated");
572       let tp = this;
573       Timers.createOneshot(10, function() tp.shutdown(tp._continueReinit), this);
574     }
575     catch (ex) {
576       Debug.log("Failed to reload any downloads from queuefile", ex);
577     }
578   },
579   _continueReinit: function() {
580     this._running = [];
581     delete this._forceQuit;
582     this._speeds.clear();
583     this.offlineForced = false;
584    
585     this._loadDownloads();
586   },
587  
588   observe: function D_observe(subject, topic, data) {
589     if (topic == 'quit-application-requested') {
590       if (!this._canClose()) {
591         delete this._forceClose;
592         try {
593           let cancelQuit = subject.QueryInterface(Ci.nsISupportsPRBool);
594           cancelQuit.data = true;
595         }
596         catch (ex) {
597           Debug.log("cannot set cancelQuit", ex);
598         }
599       }
600     }
601     else if (topic == 'quit-application-granted') {
602       this._forceClose = true;
603     }
604     else if (topic == 'network:offline-status-changed') {
605       this.offline = data == "offline";
606     }
607   },
608   refresh: function D_refresh() {
609     try {
610       const now = Utils.getTimestamp();
611       for each (let d in this._running) {
612         let advanced = d.speeds.add(d.partialSize, now);
613         this._sum += advanced;
614        
615         // Calculate estimated time
616         if (advanced != 0 && d.totalSize > 0) {
617           let remaining = Math.ceil((d.totalSize - d.partialSize) / d.speeds.avg);
618           if (!isFinite(remaining)) {
619             d.status = _("unknown");
620           }
621           else {
622             d.status = Utils.formatTimeDelta(remaining);
623           }
624         }
625         d.speed = Utils.formatSpeed(d.speeds.avg);
626         if (d.speedLimit > 0) {
627           d.speed += " (" + Utils.formatSpeed(d.speedLimit, 0) + ")";
628         }
629       }
630       this._speeds.add(this._sum, now);
631       speed = Utils.formatBytes(this._speeds.avg);
632       this._maxObservedSpeed = Math.max(this._speeds.avg, this._maxObservedSpeed);
633       for each (let e in $('listSpeeds', 'perDownloadSpeedLimitList')) {
634         e.hint = this._maxObservedSpeed;
635       }
636
637       // Refresh status bar
638       $('statusText').label = _("currentdownloads", [this.completed, Tree.rowCount, this._running.length]);
639       $('statusSpeed').label = _("currentspeed", [speed]);
640
641       // Refresh window title
642       if (this._running.length == 1 && this._running[0].totalSize > 0) {
643         document.title =
644           this._running[0].percent
645           + ' - '
646           + this.completed + "/" + Tree.rowCount + " - "
647           + $('statusSpeed').label + ' - DownThemAll!';
648       }
649       else if (this._running.length > 0) {
650         document.title =
651           Math.floor(this.completed * 100 / Tree.rowCount) + '%'
652           + ' - '       
653           + this.completed + "/" + Tree.rowCount + " - "
654           + $('statusSpeed').label + ' - DownThemAll!';
655       }
656       else {
657         document.title = this.completed + "/" + Tree.rowCount + " - DownThemAll!";
658       }
659     }
660     catch(ex) {
661       Debug.log("refresh():", ex);
662     }
663   },
664   refreshWritten: function D_refreshWritten() {
665     for each (let d in this._running) {
666       d.invalidate(1);
667       d.invalidate(2);
668       d.invalidate(3);
669     }
670   },
671   saveRunning: function D_saveRunning() {
672     if (!this._running.length) {
673       return;
674     }
675     QueueStore.beginUpdate();
676     for each (let d in this._running) {
677       d.save();
678     }
679     QueueStore.endUpdate();
680   },
681  
682   _processOfflineChange: function D__processOfflineChange() {
683     let de = $('downloads');
684     if (this.offline == de.hasAttribute('offline')) {
685       return;
686     }
687    
688     if (this.offline) {
689       de.setAttribute('offline', true);
690       $('netstatus').setAttribute('offline', true);
691       for (let d in Tree.all) {
692         if (d.is(RUNNING)) {
693           d.pause();
694           d.queue();
695         }
696       }   
697     }
698     else if (de.hasAttribute('offline')) {
699       de.removeAttribute('offline');
700       $('netstatus').removeAttribute('offline');
701     }
702     Tree.box.invalidate();   
703   },
704
705   checkDownloads: function D_checkDownloads() {
706     try {
707       this.refresh();
708      
709       for each (let d in this._running) {
710         // checks for timeout
711         if (d.is(RUNNING) && (Utils.getTimestamp() - d.timeLastProgress) >= Prefs.timeout * 1000) {
712           if (d.resumable || !d.totalSize || !d.partialSize || Prefs.resumeOnError) {
713             Dialog.markAutoRetry(d);
714             d.pause();
715             d.status = _("timeout");
716           }
717           else {
718             d.cancel(_("timeout"));
719           }
720           Debug.logString(d + " is a timeout");
721         }
722       }
723      
724       if (Prefs.autoClearComplete && this._autoClears.length) {
725         Tree.remove(this._autoClears);
726         this._autoClears = [];
727       }
728
729       if (!this.offline) {
730         if (Prefs.autoRetryInterval) {
731           this._autoRetrying = this._autoRetrying.filter(function(d) !d.autoRetry());
732         }
733         this.startNext();
734       }
735     }
736     catch(ex) {
737       Debug.log("checkDownloads():", ex);
738     }
739   },
740   checkSameName: function D_checkSameName(download, path) {
741     for each (let runner in this._running) {
742       if (runner == download) {
743         continue;
744       }
745       if (runner.destinationFile == path) {
746         return true;
747       }
748     }
749     return false;
750   },
751   startNext: function D_startNext() {
752     try {
753       var rv = false;
754       // pre-condition, do check prior to loop, or else we'll have the generator cost.
755       if (this._running.length >= Prefs.maxInProgress) {
756         return false;
757       }       
758       let gen = Limits.getScheduler(Tree.all, this._running);
759       for (let d in gen) {
760         if (!d.is(QUEUED)) {
761           Debug.logString("FIXME: scheduler returned unqueued download");
762           continue;
763         }
764         this.run(d);
765         if (this._running.length >= Prefs.maxInProgress) {
766           return true;
767         }
768         rv = true;
769       }
770       delete gen;
771       return rv;
772     }
773     catch(ex){
774       Debug.log("startNext():", ex);
775     }
776     return false;
777   },
778   run: function D_run(download) {
779     if (this.offline) {
780       return;
781     }
782     download.status = _("starting");
783     if (download.is(FINISHING) || (download.partialSize >= download.totalSize && download.totalSize)) {
784       // we might encounter renaming issues;
785       // but we cannot handle it because we don't know at which stage we crashed
786       download.partialSize = download.totalSize;
787       Debug.logString("Download seems to be complete; likely a left-over from a crash, finish it:" + download);
788       download.finishDownload();
789       return;
790     }
791     download.timeLastProgress = Utils.getTimestamp();
792     download.timeStart = Utils.getTimestamp();
793     download.state = RUNNING;
794     if (!download.started) {
795       download.started = true;
796       Debug.logString("Let's start " + download);
797     }
798     else {
799       Debug.logString("Let's resume " + download + " at " + download.partialSize);
800     }
801     this._running.push(download);
802     download.prealloc();
803     download.resumeDownload();
804   },
805   wasStopped: function D_wasStopped(download) {
806     this._running = this._running.filter(function (d) d != download);
807   },
808   signal: function D_signal(download) {
809     download.save();
810     if (download.is(RUNNING)) {
811       this._wasRunning = true;
812     }
813     else if (Prefs.autoClearComplete && download.is(COMPLETE)) {
814       this._autoClears.push(download);
815     }
816     if (!this._initialized || !this._wasRunning || !download.is(COMPLETE)) {
817       return;
818     }
819     try {
820       // check if there is something running or scheduled
821       if (this.startNext() || Tree.some(function(d) { return d.isOf(FINISHING, RUNNING, QUEUED); } )) {
822         return;
823       }
824       Debug.logString("signal(): Queue finished");
825       Utils.playSound("done");
826      
827       let dp = Tree.at(0);
828       if (dp) {
829         dp = dp.destinationPath;
830       }
831       if (Prefs.alertingSystem == 1) {
832         AlertService.show(_("dcom"), _('suc'), dp, dp);
833       }
834       else if (dp && Prefs.alertingSystem == 0) {
835         if (confirm(_('suc') + "\n "+ _("folder")) == 1) {
836           try {
837             OpenExternal.launch(dp);
838           }
839           catch (ex){
840             // no-op
841           }
842         }
843       }
844       if (Prefs.autoClose) {
845         Dialog.close();
846       }
847     }
848     catch(ex) {
849       Debug.log("signal():", ex);
850     }
851   },
852   markAutoRetry: function D_markAutoRetry(d) {
853     d.initAutoRetry();
854     if (this._autoRetrying.indexOf(d) == -1) {
855       this._autoRetrying.push(d);
856     }
857   },
858   wasRemoved: function D_wasRemoved(d) {
859     this._running = this._running.filter(function(r) r != d);
860     this._autoRetrying = this._autoRetrying.filter(function(r) r != d);
861   },
862   _canClose: function D__canClose() {
863     if (Tree.some(function(d) { return d.started && !d.resumable && d.isOf(RUNNING); })) {
864       var rv = Prompts.confirmYN(
865         window,
866         _("confclose"),
867         _("nonresclose")
868       );
869       if (rv) {
870         return false;
871       }
872     }
873     return (this._forceClose = true);
874   },
875   close: function() {
876     this.shutdown(this._doneClosing);
877   },
878   _doneClosing: function() {
879     closeWindow(true);
880   },
881   shutdown: function D_close(callback) {
882     Debug.logString("Close request");
883     if (!this._initialized) {
884       Debug.logString("not initialized. Going down immediately!");
885       return true;
886     }
887     if (!this._forceClose && !this._canClose()) {
888       delete this._forceClose;
889       Debug.logString("Not going to close!");
890       return false;
891     }
892     this.offlineForced = true;
893
894     // stop everything!
895     // enumerate everything we'll have to wait for!
896     if (this._updTimer) {
897       Timers.killTimer(this._updTimer);
898       delete this._updTimer;
899     }
900    
901     let chunks = 0;
902     let finishing = 0;
903     Debug.logString("Going to close all");
904     Tree.updateAll(
905       function(d) {
906         if (d.isOf(RUNNING, QUEUED)) {
907           // enumerate all running chunks
908           d.chunks.forEach(
909             function(c) {
910               if (c.running) {
911                 ++chunks;
912               }
913             },
914             this
915           );
916           d.pause();
917           d.state = QUEUED;       
918         }
919         else if (d.is(FINISHING)) {
920           ++finishing;
921         }
922         d.cancelPreallocation();
923         return true;
924       },
925       this
926     );
927     Debug.logString("Still running: " + chunks + " Finishing: " + finishing);
928     if (chunks || finishing) {
929       if (this._safeCloseAttempts < 20) {
930         ++this._safeCloseAttempts;
931         let tp = this;
932         Timers.createOneshot(250, function() tp.shutdown(callback), this);       
933         return false;
934       }
935       Debug.logString("Going down even if queue was not probably closed yet!");
936     }
937     callback.call(this);
938     return true;
939   },
940   _cleanTmpDir: function D__cleanTmpDir() {
941     if (!Prefs.tempLocation || Preferences.getExt("tempLocation", '') != '') {
942       // cannot perform this action if we don't use a temp file
943       // there might be far too many directories containing far too many
944       // tmpFiles.
945       // or part files from other users.
946       return;
947     }
948     let known = [];
949     for (d in Tree.all) {
950       known.push(d.tmpFile.leafName);
951     }
952     let tmpEnum = Prefs.tempLocation.directoryEntries;
953     let unknown = [];
954     for (let f in new Utils.SimpleIterator(tmpEnum, Ci.nsILocalFile)) {
955       if (f.leafName.match(/\.dtapart$/) && known.indexOf(f.leafName) == -1) {
956         unknown.push(f);
957       }
958     }
959     unknown.forEach(
960       function(f) {
961         try {
962           f.remove(false);
963         }
964         catch(ex) {
965         }
966       }
967     );
968   },
969   _safeCloseAttempts: 0,
970
971   unload: function D_unload() {
972     PrivateBrowsing.unregisterCallbacks(this);
973     GlobalBucket.kill();
974     Limits.killServerBuckets();
975    
976     Timers.killAllTimers();
977     if (this._loader) {
978       this._loader.cancel();
979     }
980     Prefs.shutdown();
981     try {
982       this._cleanTmpDir();
983     }
984     catch(ex) {
985       Debug.log("_safeClose", ex);
986     }
987     for each (let w in this._infoWindows) {
988       if (!w.closed) {
989         w.close();
990       }
991     }
992     return true;   
993   }
994 };
995 addEventListener('load', function() Dialog.init(), false);
996
997 const Metalinker = {
998   handleDownload: function ML_handleDownload(download) {
999     download.state = CANCELED;
1000     Tree.remove(download, false);
1001     let file = new FileFactory(download.destinationFile);
1002    
1003     this.handleFile(file, download.referrer);
1004    
1005     try {
1006       file.remove(false);
1007     }
1008     catch (ex) {
1009       Debug.log("failed to remove metalink file!", ex);
1010     }
1011   },
1012   handleFile: function ML_handleFile(aFile, aReferrer) {
1013     try {
1014       let res = this.parse(aFile, aReferrer);
1015       if (!res.downloads.length) {
1016         throw new Error(_('mlnodownloads'));
1017       }
1018       res.downloads.forEach(function(e) {
1019         e.size = Utils.formatBytes(e.size);
1020         e.fileName = e.fileName.getUsableFileName();
1021       });
1022       window.openDialog(
1023         'chrome://dta/content/dta/manager/metaselect.xul',
1024         '_blank',
1025         'chrome,centerscreen,dialog=yes,modal',
1026         res.downloads,
1027         res.info
1028       );
1029       res.downloads = res.downloads.filter(function(d) { return d.selected; });
1030       if (res.downloads.length) {
1031         startDownloads(res.info.start, res.downloads);
1032       }
1033     }
1034     catch (ex) {
1035       Debug.log("Metalinker::handleDownload", ex);     
1036       if (!(ex instanceof Error)) {
1037         ex = new Error(_('mlerror', [ex.message ? ex.message : (ex.error ? ex.error : ex.toString())]));
1038       }
1039       if (ex instanceof Error) {
1040         AlertService.show(_('mlerrortitle'), ex.message, false);
1041       }
1042     }
1043   }
1044 };
1045 module('resource://dta/metalinker.jsm', Metalinker);
1046
1047
1048 function QueueItem(lnk, dir, num, desc, mask, referrer, tmpFile) {
1049
1050   this.visitors = new VisitorManager();
1051
1052   this.startDate = new Date(); 
1053
1054   this.chunks = [];
1055   this.speeds = new SpeedStats(SPEED_COUNT);
1056 }
1057
1058 QueueItem.prototype = {
1059   _state: QUEUED,
1060   get state() {
1061     return this._state;
1062   },
1063   set state(nv) {
1064     if (this._state == nv) {
1065       return nv;
1066     }
1067     if (this._state == RUNNING) {
1068       // remove ourself from inprogresslist
1069       Dialog.wasStopped(this);
1070       // kill the bucket via it's setter
1071       this.bucket = null;
1072     }
1073     this.speed = '';
1074     this._state = nv;
1075     if (this._state == RUNNING) {
1076       // set up the bucket
1077       this._bucket = new ByteBucket(this.speedLimit, 1.7);
1078     }   
1079     Dialog.signal(this);
1080     this.invalidate();
1081     Tree.refreshTools();
1082     return nv;
1083   },
1084  
1085   _bucket: null,
1086   get bucket() {
1087     return this._bucket;
1088   },
1089   set bucket(nv) {
1090     if (nv !== null) {
1091       throw new Exception("Bucket is only nullable");
1092     }
1093     if (this._bucket) {
1094       this._bucket.kill();
1095       this._bucket = null;
1096     }
1097   },
1098  
1099   _speedLimit: -1,
1100   get speedLimit() {
1101     return this._speedLimit;
1102   },
1103   set speedLimit(nv) {
1104     nv = Math.max(nv, -1);
1105     if (this._speedLimit == nv) {
1106       return;
1107     }
1108     this._speedLimit = nv;
1109     if (this.is(RUNNING)) {
1110       this._bucket.byteRate = this.speedLimit;
1111     }
1112     this.save();
1113   },
1114  
1115   postData: null,
1116  
1117   fromMetalink: false,
1118   numIstance: 0,
1119  
1120   _fileName: null,
1121   get fileName() {
1122     return this._fileName;
1123   },
1124   set fileName(nv) {
1125     if (this._fileName == nv) {
1126       return nv;
1127     }
1128     this._fileName = nv;
1129     this.rebuildDestination();
1130     this.invalidate(0);
1131     return nv;
1132   },
1133   _description: null,
1134   get description() {
1135     return this._description;
1136   },
1137   set description(nv) {
1138     if (nv == this._description) {
1139       return nv;
1140     }
1141     this._description = nv;
1142     this.rebuildDestination();
1143     this.invalidate(0);
1144     return nv;
1145   }, 
1146   _title: '',
1147   get title() {
1148     return this._title;
1149   },
1150   set title(nv) {
1151     if (nv == this._title) {
1152       return this._title;
1153     }
1154     this._title = nv;
1155     this.rebuildDestination();
1156     this.invalidate(0);
1157     return this._title;
1158   },
1159   _pathName: null,
1160   get pathName() {
1161     return this._pathName;
1162   },
1163   set pathName(nv) {
1164     nv = nv.toString();
1165     if (this._pathName == nv) {
1166       return nv;
1167     }
1168     this._pathName = nv;
1169     this.rebuildDestination();
1170     this.invalidate(0);
1171     return nv;
1172   }, 
1173
1174   _mask: null,
1175   get mask() {
1176     return this._mask;
1177   },
1178   set mask(nv) {
1179     if (this._mask == nv) {
1180       return nv;
1181     }
1182     this._mask = nv;
1183     this.rebuildDestination();
1184     this.invalidate(7);
1185     return nv;
1186   },   
1187  
1188   _destinationName: null,
1189   destinationNameOverride: null,
1190   _destinationNameFull: null,
1191   get destinationName() {
1192     return this._destinationNameFull;
1193   },
1194   set destinationName(nv) {
1195     if (this.destinationNameOverride == nv) {
1196       return this._destinationNameFull;
1197     }
1198     this.destinationNameOverride = nv;
1199     this.rebuildDestination();
1200     this.invalidate(0);
1201     return this._destinationNameFull;
1202   },
1203  
1204   _destinationFile: null,
1205   get destinationFile() {
1206     if (!this._destinationFile) {
1207       this.rebuildDestination();
1208     }
1209     return this._destinationFile;
1210   },
1211  
1212   _conflicts: 0,
1213   get conflicts() {
1214     return this._conflicts;
1215   },
1216   set conflicts(nv) {
1217     if (this._conflicts == nv) {
1218       return nv;
1219     }
1220     this._conflicts = nv;
1221     this.rebuildDestination();
1222     this.invalidate(0);
1223     return nv;
1224   },
1225   _tmpFile: null,
1226   get tmpFile() {
1227     if (!this._tmpFile) {
1228       var dest = Prefs.tempLocation
1229         ? Prefs.tempLocation.clone()
1230         : new FileFactory(this.destinationPath);
1231       let name = this.fileName;
1232       if (name.length > 60) {
1233         name = name.substring(0, 60);
1234       }
1235       dest.append(name + "-" + Utils.newUUIDString() + '.dtapart');
1236       this._tmpFile = dest;
1237     }
1238     return this._tmpFile;
1239   },
1240   _hashCollection: null,
1241   get hashCollection() {
1242     return this._hashCollection;
1243   },
1244   set hashCollection(nv) {
1245     if (nv != null && !(nv instanceof DTA.HashCollection)) {
1246       throw new Exception("Not a hash collection");
1247     }
1248     this._hashCollection = nv;
1249     this._prettyHash = this._hashCollection
1250       ? _('prettyhash', [this._hashCollection.full.type, this._hashCollection.full.sum])
1251       : _('nas');
1252   },
1253   _prettyHash: null,
1254   get prettyHash() {
1255     return this._prettyHash;
1256   },
1257
1258   /**
1259    * Takes one or more state indicators and returns if this download is in state
1260    * of any of them
1261    */
1262   is: function QI_is(state) {
1263     return this._state == state;
1264   },
1265   isOf: function QI_isOf() {
1266     let state = this._state;
1267     for (let i = 0, e = arguments.length; i < e; ++i) {
1268       if (state == arguments[i]) {
1269         return true;
1270       }
1271     }
1272     return false;   
1273   },
1274  
1275   save: function QI_save() {
1276     if (
1277       (Prefs.removeCompleted && this.is(COMPLETE))
1278       || (Prefs.removeCanceled && this.is(CANCELED))
1279       || (Prefs.removeAborted && this.is(PAUSED))
1280     ) {
1281       if (this.dbId) {
1282         this.remove();
1283       }
1284       return false;     
1285     }     
1286     if (this.dbId) {
1287       QueueStore.saveDownload(this.dbId, this.toSource());
1288       return true;
1289     }
1290
1291     this.dbId = QueueStore.addDownload(this.toSource(), this.position);
1292     return true;
1293   },
1294   remove: function QI_remove() {
1295     QueueStore.deleteDownload(this.dbId);
1296     delete this.dbId;
1297   },
1298   _position: -1,
1299   get position() {
1300     return this._position;
1301   },
1302   set position(nv) {
1303     if (nv == this._position) {
1304       return;
1305     }
1306     this._position = nv;
1307     if (this.dbId && this._position != -1) {
1308       QueueStore.savePosition(this.dbId, this._position);
1309     }
1310   },
1311
1312   contentType: "",
1313   visitors: null,
1314   _totalSize: 0,
1315   get totalSize() { return this._totalSize; },
1316   set totalSize(nv) {
1317     if (nv >= 0 && !isNaN(nv)) {
1318       this._totalSize = Math.floor(nv);
1319     }
1320     this.invalidate(3);
1321     this.prealloc();
1322     return this._totalSize;
1323   },
1324   partialSize: 0,
1325
1326   startDate: null,
1327
1328   compression: null,
1329
1330   resumable: true,
1331   started: false,
1332
1333   _activeChunks: 0,
1334   get activeChunks() {
1335     return this._activeChunks;
1336   },
1337   set activeChunks(nv) {
1338     nv = Math.max(0, nv);
1339     this._activeChunks = nv;
1340     this.invalidate(6);
1341     return this._activeChunks;
1342   },
1343   _maxChunks: 0,
1344   get maxChunks() {
1345     if (!this._maxChunks) {
1346         this._maxChunks = Prefs.maxChunks;
1347     }
1348     return this._maxChunks;
1349   },
1350   set maxChunks(nv) {
1351     this._maxChunks = nv;
1352     if (this._maxChunks < this._activeChunks) {
1353       let running = this.chunks.filter(function(c) { return c.running; });
1354       while (running.length && this._maxChunks < running.length) {
1355         let c = running.pop();
1356         if (c.remainder < 10240) {
1357           continue;
1358         }
1359         c.cancel();
1360       }
1361     }
1362     else if (this._maxChunks > this._activeChunks && this.is(RUNNING)) {
1363       this.resumeDownload();
1364      
1365     }
1366     this.invalidate(6);
1367     Debug.logString("mc set to " + nv);
1368     return this._maxChunks;
1369   },
1370   timeLastProgress: 0,
1371   timeStart: 0,
1372
1373   _icon: null,
1374   get icon() {
1375     if (!this._icon) {
1376       this._icon = getIcon(this.destinationName, 'metalink' in this);
1377     }
1378     return this._icon;
1379   },
1380   get largeIcon() {
1381     return getIcon(this.destinationName, 'metalink' in this, 32);
1382   },
1383   get size() {
1384     try {
1385       let file = null;
1386       if (!this.isOf(COMPLETE, FINISHING)) {
1387         file = this._tmpFile || null;
1388       }
1389       else {
1390         file = new FileFactory(this.destinationFile);
1391       }
1392       if (file && file.exists()) {
1393         return file.fileSize;
1394       }
1395     }
1396     catch (ex) {
1397       Debug.log("download::getSize(): ", ex);
1398     }
1399     return 0;
1400   },
1401   get dimensionString() {
1402     if (this.partialSize <= 0) {
1403       return _('unknown');
1404     }
1405     else if (this.totalSize <= 0) {
1406       return _('transfered', [Utils.formatBytes(this.partialSize), _('nas')]);
1407     }
1408     else if (this.is(COMPLETE)) {
1409       return Utils.formatBytes(this.totalSize);
1410     }
1411     return _('transfered', [Utils.formatBytes(this.partialSize), Utils.formatBytes(this.totalSize)]);
1412   },
1413   _status : '',
1414   get status() {
1415     if (Dialog.offline && this.isOf(QUEUED, PAUSED)) {
1416       return _('offline');
1417     }
1418     return this._status + (this.autoRetrying ? ' *' : '');
1419   },
1420   set status(nv) {
1421     if (nv != this._status) {
1422       this._status = nv;
1423       this.invalidate();
1424     }
1425     return this._status;
1426   },
1427   get parts() {
1428     if (this.maxChunks) {
1429       return (this.activeChunks) + '/' + this.maxChunks;
1430     }
1431     return '';
1432   },
1433   get percent() {
1434     if (!this.totalSize && this.is(RUNNING)) {
1435       return _('nas');
1436     }
1437     else if (!this.totalSize) {
1438       return "0%";
1439     }
1440     else if (this.is(COMPLETE)) {
1441       return "100%";
1442     }
1443     return Math.floor(this.partialSize / this.totalSize * 100) + "%";
1444   },
1445   _destinationPath: '',
1446   get destinationPath() {
1447     return this._destinationPath;
1448   },
1449
1450   invalidate: function QI_invalidate(cell) {
1451     Tree.invalidate(this, cell);
1452   },
1453
1454   safeRetry: function QI_safeRetry() {
1455     // reset flags
1456     this.totalSize = this.partialSize = 0;
1457     this.compression = null;
1458     this.activeChunks = this.maxChunks = 0;
1459     this.chunks.forEach(function(c) { c.cancel(); });
1460     this.chunks = [];
1461     this.speeds.clear();
1462     this.visitors = new VisitorManager();
1463     this.state = QUEUED;
1464     Dialog.run(this);
1465   },
1466
1467   refreshPartialSize: function QI_refreshPartialSize(){
1468     let size = 0;
1469     this.chunks.forEach(function(c) { size += c.written; });
1470     this.partialSize = size;
1471   },
1472
1473   pause: function QI_pause(){
1474     if (this.chunks) {
1475       for each (let c in this.chunks) {
1476         if (c.running) {
1477           c.cancel();
1478         }
1479       }
1480     }
1481     this.activeChunks = 0;
1482     this.state = PAUSED;
1483     this.speeds.clear();
1484   },
1485
1486   moveCompleted: function QI_moveCompleted() {
1487     if (this.is(CANCELED)) {
1488       return;
1489     }
1490     ConflictManager.resolve(this, 'continueMoveCompleted');
1491   },
1492   continueMoveCompleted: function QI_continueMoveCompleted() {
1493     if (this.is(CANCELED)) {
1494       return;
1495     }   
1496     try {
1497       // safeguard against some failed chunks.
1498       this.chunks.forEach(function(c) { c.close(); });
1499       var destination = new FileFactory(this.destinationPath);
1500       Debug.logString(this.fileName + ": Move " + this.tmpFile.path + " to " + this.destinationFile);
1501
1502       if (!destination.exists()) {
1503         destination.create(Ci.nsIFile.DIRECTORY_TYPE, Prefs.dirPermissions);
1504       }
1505       var df = destination.clone();
1506       df.append(this.destinationName);
1507       if (df.exists()) {
1508         df.remove(false);
1509       }
1510       // move file
1511       if (this.compression) {
1512         this.state = FINISHING;
1513         this.status =  _("decompress");
1514         new Decompressor(this);
1515       }
1516       else {
1517         this.tmpFile.clone().moveTo(destination, this.destinationName);
1518         this.complete();
1519       }
1520     }
1521     catch(ex) {
1522       Debug.log("continueMoveCompleted encountered an error", ex);
1523       this.complete(ex);
1524     }
1525   },
1526   handleMetalink: function QI_handleMetaLink() {
1527     try {
1528       Metalinker.handleDownload(this);
1529     }
1530     catch (ex) {
1531       Debug.log("handleMetalink", ex);
1532     }
1533   },
1534   verifyHash: function() {
1535     this.state = FINISHING;
1536     this.status = _("verify");
1537     new Verificator(this, this.verifyHashOk, this.verifyHashError);
1538   },
1539   verifyHashOk: function() {
1540     this.complete();
1541   },
1542   verifyHashError: function() {
1543     let file = new FileFactory(this.destinationFile);
1544     function deleteFile() {
1545       try {
1546         if (file.exists()) {
1547           file.remove(false);
1548         }
1549       }
1550       catch (ex) {
1551         Debug.log("Failed to remove file after checksum mismatch", ex);
1552       }
1553     }
1554     let act = Prompts.confirm(window, _('verifyerrortitle'), _('verifyerrortext'), _('retry'), _('delete'), _('keep'));
1555     switch (act) {
1556       case 0: deleteFile(); this.safeRetry(); return;
1557       case 1: deleteFile(); this.cancel(); return;
1558     }
1559     this.verifyHashOk();
1560   },   
1561   customFinishEvent: function() {
1562     DTA_include("dta/manager/customevent.js");
1563     new CustomEvent(this, Prefs.finishEvent);
1564   },
1565   setAttributes: function() {
1566     if (Prefs.setTime) {
1567       try {
1568         let time = this.startDate.getTime();
1569         try {
1570           time =  this.visitors.time;
1571         }
1572         catch (ex) {
1573           // no-op
1574         }
1575         // small validation. Around epoche? More than a month in future?
1576         if (time < 2 || time > Date.now() + 30 * 86400000) {
1577           throw new Exception("invalid date encountered: " + time + ", will not set it");
1578         }
1579         // have to unwrap
1580         let file = new FileFactory(this.destinationFile);
1581         file.lastModifiedTime = time;
1582       }
1583       catch (ex) {
1584         Debug.log("Setting timestamp on file failed: ", ex);
1585       }
1586     }
1587     this.totalSize = this.partialSize = this.size;
1588     ++Dialog.completed;
1589    
1590     this.complete();
1591   },
1592   finishDownload: function QI_finishDownload(exception) {
1593     Debug.logString("finishDownload, connections: " + this.sessionConnections);
1594     this._completeEvents = ['moveCompleted', 'setAttributes'];
1595     if (this.hashCollection) {
1596       this._completeEvents.push('verifyHash');
1597     }
1598     if ('isMetalink' in this) {
1599       this._completeEvents.push('handleMetalink');
1600     }
1601     if (Prefs.finishEvent) {
1602       this._completeEvents.push('customFinishEvent');
1603     }
1604     this.complete();
1605   },
1606   _completeEvents: [],
1607   complete: function QI_complete(exception) {
1608     this.chunks = [];
1609     this.speeds.clear();
1610     if (exception) {
1611       this.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
1612       Debug.log("complete: ", exception);
1613       return;
1614     }
1615     if (this._completeEvents.length) {
1616       var evt = this._completeEvents.shift();
1617       var tp = this;
1618       window.setTimeout(
1619         function() {
1620           try {
1621             tp[evt]();
1622           }
1623           catch(ex) {
1624             Debug.log("completeEvent failed: " + evt, ex);
1625             tp.complete();
1626           }
1627         },
1628         0
1629       );
1630       return;
1631     }
1632     this.activeChunks = 0;
1633     this.state = COMPLETE;
1634     this.status = TEXT_COMPLETE;
1635     this.visitors = new VisitorManager();
1636   },
1637   rebuildDestination: function QI_rebuildDestination() {
1638     try {
1639       let uri = this.urlManager.usable.toURL();
1640       let host = uri.host.toString();
1641
1642       // normalize slashes
1643       let mask = this.mask
1644         .normalizeSlashes()
1645         .removeLeadingSlash()
1646         .removeFinalSlash();
1647
1648       let uripath = uri.path.removeLeadingChar("/");
1649       if (uripath.length) {
1650         uripath = uripath.substring(0, uri.path.lastIndexOf("/"))
1651           .normalizeSlashes()
1652           .removeFinalSlash();
1653       }
1654
1655       let query = '';
1656       try {
1657         query = uri.query;
1658       }
1659       catch (ex) {
1660         // no-op
1661       }
1662
1663       let description = this.description.removeBadChars().replaceSlashes(' ').trim();
1664       let title = this.title.removeBadChars().trim();
1665      
1666       let name = this.fileName;
1667       let ext = name.getExtension();
1668       if (ext) {
1669         name = name.substring(0, name.length - ext.length - 1);
1670
1671         if (this.contentType && /htm/.test(this.contentType) && !/htm/.test(ext)) {
1672           ext += ".html";
1673         }
1674       }
1675       // mime-service method
1676       else if (this.contentType && /^(?:image|text)/.test(this.contentType)) {
1677         try {
1678           let info = MimeService.getFromTypeAndExtension(this.contentType.split(';')[0], "");
1679           ext = info.primaryExtension;
1680         } catch (ex) {
1681           ext = '';
1682         }
1683       }
1684       else {
1685         name = this.fileName;
1686         ext = '';
1687       }
1688       let ref = this.referrer ? this.referrer.host.toString() : '';
1689      
1690       let curl = (uri.host + ((uripath=="") ? "" : (SYSTEMSLASH + uripath)));
1691      
1692       var replacements = {
1693         "name": name,
1694         "ext": ext,
1695         "text": description,
1696         "flattext": description.replaceSlashes(Prefs.flatReplacementChar).replace(/[\n\r\s]+/g, ' ').trim(),
1697         'title': title,
1698         'flattitle': title.replaceSlashes(Prefs.flatReplacementChar).replace(/[\n\r\s]+/g, ' ').trim(),
1699         "url": host,
1700         "subdirs": uripath,
1701         "flatsubdirs": uripath.replaceSlashes(Prefs.flatReplacementChar).trim(),
1702         "refer": ref,
1703         "qstring": query,
1704         "curl": curl,
1705         "flatcurl": curl.replaceSlashes(Prefs.flatReplacementChar),
1706         "num": Utils.formatNumber(this.numIstance),
1707         "hh": Utils.formatNumber(this.startDate.getHours(), 2),
1708         "mm": Utils.formatNumber(this.startDate.getMinutes(), 2),
1709         "ss": Utils.formatNumber(this.startDate.getSeconds(), 2),
1710         "d": Utils.formatNumber(this.startDate.getDate(), 2),
1711         "m": Utils.formatNumber(this.startDate.getMonth() + 1, 2),
1712         "y": String(this.startDate.getFullYear())
1713       }
1714       function replacer(type) {
1715         let t = type.substr(1, type.length - 2);
1716         if (t in replacements) {
1717           return replacements[t];
1718         }
1719         return type;
1720       }
1721      
1722       mask = mask.replace(/\*\w+\*/gi, replacer);
1723
1724       mask = mask.removeBadChars().removeFinalChar(".").trim().split(SYSTEMSLASH);
1725       let file = new FileFactory(this.pathName.addFinalSlash());
1726       while (mask.length) {
1727         file.append(mask.shift());
1728       }
1729       this._destinationName = file.leafName;
1730       this._destinationPath = file.parent.path;
1731     }
1732     catch(ex) {
1733       this._destinationName = this.fileName;
1734       this._destinationPath = this.pathName.addFinalSlash();
1735       Debug.log("rebuildDestination():", ex);
1736     }
1737     this._destinationNameFull = Utils.formatConflictName(
1738       this.destinationNameOverride ? this.destinationNameOverride : this._destinationName,
1739       this.conflicts
1740     );
1741     let file = new FileFactory(this.destinationPath);
1742     file.append(this.destinationName);
1743     this._destinationFile = file.path;
1744     this._icon = null;
1745   },
1746
1747   fail: function QI_fail(title, msg, state) {
1748     Debug.logString("failDownload invoked");
1749
1750     this.cancel(state);
1751
1752     Utils.playSound("error");
1753
1754     switch (Prefs.alertingSystem) {
1755       case 1:
1756         AlertService.show(title, msg, false);
1757         break;
1758       case 0:
1759         alert(msg);
1760         break;
1761     }
1762   },
1763
1764   cancel: function QI_cancel(message) {
1765     try {
1766       if (this.is(CANCELED)) {
1767         return;
1768       }
1769       if (this.is(COMPLETE)) {
1770         Dialog.completed--;
1771       }
1772       else if (this.is(RUNNING)) {
1773         this.pause();
1774       }
1775       this.state = CANCELED;     
1776       Debug.logString(this.fileName + ": canceled");
1777
1778       this.visitors = new VisitorManager();
1779
1780       if (message == "" || !message) {
1781         message = _("canceled");
1782       }
1783       this.status = message;
1784      
1785       this.cancelPreallocation();
1786      
1787       this.removeTmpFile();
1788
1789       // gc
1790       this.chunks = [];
1791       this.totalSize = this.partialSize = 0;
1792       this.maxChunks = this.activeChunks = 0;
1793       this.conflicts = 0;
1794       this.resumable = true;
1795       this._autoRetries = 0;
1796       delete this._autoRetryTime;
1797       this.save();
1798     }
1799     catch(ex) {
1800       Debug.log("cancel():", ex);
1801     }
1802   },
1803  
1804   prealloc: function QI_prealloc() {
1805     let file = this.tmpFile;
1806    
1807     if (!this.is(RUNNING)) {
1808       return false;
1809     }
1810    
1811     if (!this.totalSize) {
1812       Debug.logString("pa: no totalsize");
1813       return false;
1814     }
1815     if (this.preallocating) {
1816       Debug.logString("pa: already working");
1817       return true;
1818     }
1819    
1820     if (!file.exists() || this.totalSize != this.size) {
1821       if (!file.parent.exists()) {
1822         file.parent.create(Ci.nsIFile.DIRECTORY_TYPE, Prefs.dirPermissions);
1823       }
1824       let pa = Preallocator.prealloc(file, this.totalSize, Prefs.permissions, this._donePrealloc, this);
1825       if (pa) {
1826         this.preallocating = true;
1827         this._preallocator = pa;
1828         Debug.logString("pa: started");
1829       }
1830     }
1831     else {
1832       Debug.logString("pa: already allocated");
1833     }
1834     return this.preallocating;
1835   },
1836   cancelPreallocation: function() {
1837     Debug.logString("pa: cancel requested");
1838     if (this._preallocator) {
1839       Debug.logString("pa: going to cancel");
1840       this._preallocator.cancel();
1841       delete this._preallocator;
1842       this._preallocator = null;
1843       Debug.logString("pa: cancelled");
1844     }
1845     this.preallocating = false;
1846   },
1847  
1848   _donePrealloc: function QI__donePrealloc(res) {
1849     Debug.logString("pa: done");
1850     delete this._preallocator;
1851     this._preallocator = null;
1852     this.preallocating = false;
1853     if (this.is(RUNNING)) {
1854       this.resumeDownload();
1855     }
1856   },
1857  
1858  
1859   removeTmpFile: function QI_removeTmpFile() {
1860     if (!!this._tmpFile && this._tmpFile.exists()) {
1861       try {
1862         this._tmpFile.remove(false);
1863       }
1864       catch (ex) {
1865         Debug.log("failed to remove tmpfile: " + this.tmpFile.path, ex);
1866       }
1867     }
1868     this._tmpFile = null;
1869   },
1870  
1871   sessionConnections: 0,
1872   _autoRetries: 0,
1873   _autoRetryTime: 0,
1874   get autoRetrying() {
1875     return !!this._autoRetryTime;
1876   },
1877   initAutoRetry: function QI_markRetry() {
1878     if (!Prefs.autoRetryInterval || (Prefs.maxAutoRetries && Prefs.maxAutoRetries <= this._autoRetries)) {
1879        return;
1880     }
1881     this._autoRetryTime = Utils.getTimestamp();
1882     Debug.logString("marked auto-retry: " + this);
1883     this.save();
1884   },
1885   autoRetry: function QI_autoRetry() {
1886     if (!this.autoRetrying || Utils.getTimestamp() - (Prefs.autoRetryInterval * 1000) < this._autoRetryTime) {
1887       return false;
1888     }
1889
1890     this._autoRetryTime = 0;
1891     ++this._autoRetries;
1892     this.queue();
1893     Debug.logString("Requeued due to auto-retry: " + this);
1894     return true;
1895   },
1896   queue: function QI_queue() {
1897     this._autoRetryTime = 0;
1898     this.state = QUEUED;
1899     this.status = TEXT_QUEUED;
1900   },
1901   resumeDownload: function QI_resumeDownload() {
1902     Debug.logString("resumeDownload: " + this);
1903     function cleanChunks(d) {
1904       // merge finished chunks together, so that the scoreboard does not bloat
1905       // that much
1906       for (let i = d.chunks.length - 2; i > -1; --i) {
1907         let c1 = d.chunks[i], c2 = d.chunks[i + 1];
1908         if (c1.complete && c2.complete) {
1909           c1.merge(c2);
1910           d.chunks.splice(i + 1, 1);
1911         }
1912       }
1913     }
1914     function downloadNewChunk(download, start, end, header) {
1915       var chunk = new Chunk(download, start, end);
1916       download.chunks.push(chunk);
1917       download.chunks.sort(function(a,b) { return a.start - b.start; });
1918       downloadChunk(download, chunk, header);
1919     }
1920     function downloadChunk(download, chunk, header) {
1921       chunk.running = true;
1922       download.state = RUNNING;
1923       Debug.logString("started: " + chunk);
1924       chunk.download = new Connection(download, chunk, header);
1925       ++download.activeChunks;
1926       ++download.sessionConnections;
1927     }
1928    
1929     cleanChunks(this);
1930
1931     try {
1932       if (Dialog.offline || this.maxChunks <= this.activeChunks) {
1933         return false;
1934       }
1935
1936       var rv = false;
1937
1938       // we didn't load up anything so let's start the main chunk (which will
1939       // grab the info)
1940       if (this.chunks.length == 0) {
1941         downloadNewChunk(this, 0, 0, true);
1942         this.sessionConnections = 0;       
1943         return false;
1944       }
1945      
1946      
1947       // start some new chunks
1948       let paused = this.chunks.filter(function (chunk) !(chunk.running || chunk.complete));
1949      
1950       while (this.activeChunks < this.maxChunks) {
1951         if (this.preallocating && this.activeChunks) {
1952           Debug.logString("not resuming download " + this + " because preallocating");
1953           return true;
1954         }
1955        
1956         // restart paused chunks
1957         if (paused.length) {
1958           downloadChunk(this, paused.shift());
1959           rv = true;
1960           continue;
1961         }
1962
1963         if (this.chunks.length == 1 && !!Prefs.loadEndFirst && this.chunks[0].remainder > 3 * Prefs.loadEndFirst) {
1964           // we should download the end first!
1965           let c = this.chunks[0];
1966           let end = c.end;
1967           c.end -= Prefs.loadEndFirst;
1968           downloadNewChunk(this, c.end + 1, end);         
1969           rv = true;
1970           continue;
1971         }
1972        
1973         // find biggest chunk
1974         let biggest = null;
1975         for each (let chunk in this.chunks) {
1976           if (chunk.running && chunk.remainder > MIN_CHUNK_SIZE * 2) {
1977             if (!biggest || biggest.remainder < chunk.remainder) {
1978               biggest = chunk;
1979             }
1980           }
1981         }
1982
1983         // nothing found, break
1984         if (!biggest) {
1985           break;
1986         }
1987         let end = biggest.end;
1988         biggest.end = biggest.start + biggest.written + Math.floor(biggest.remainder / 2);
1989         downloadNewChunk(this, biggest.end + 1, end);
1990         rv = true;
1991       }
1992
1993       return rv;
1994     }
1995     catch(ex) {
1996       Debug.log("resumeDownload():", ex);
1997     }
1998     return false;
1999   },
2000   dumpScoreboard: function QI_dumpScoreboard() {
2001     let scoreboard = '';
2002     let len = this.totalSize.toString().length;
2003     this.chunks.forEach(
2004       function(c,i) {
2005         scoreboard += i
2006           + ": "
2007           + c
2008           + "\n";
2009       }
2010     );
2011     Debug.logString("scoreboard\n" + scoreboard);
2012   }, 
2013   toString: function() this.urlManager.usable,
2014   toSource: function() {
2015     let e = {};
2016     [
2017       'fileName',
2018       'postData',
2019       'numIstance',
2020       'description',
2021       'title',
2022       'resumable',
2023       'mask',
2024       'pathName',
2025       'compression',
2026       'maxChunks',
2027       'contentType',
2028       'conflicts',
2029       'fromMetalink',
2030       'speedLimit'
2031     ].forEach(
2032       function(u) {
2033         // only save what is changed
2034         if (this.__proto__[u] !== this[u]) {
2035           e[u] = this[u];
2036         }
2037       },
2038       this
2039     );
2040     if (this.hashCollection) {
2041       e.hashCollection = this.hashCollection.toSource();
2042     }
2043     if (this.autoRetrying || this.is(RUNNING)) {
2044       e.state = QUEUED;
2045     }
2046     else {
2047       e.state = this.state;
2048     }
2049     if (this.destinationNameOverride) {
2050       e.destinationName = this.destinationNameOverride;
2051     }
2052     if (this.referrer) {
2053       e.referrer = this.referrer.spec;
2054     }
2055     // Store this so we can later resume.
2056     if (!this.isOf(CANCELED, COMPLETE) && this.partialSize) {
2057       e.tmpFile = this.tmpFile.path;
2058     }
2059     e.startDate = this.startDate.getTime();
2060
2061     e.urlManager = this.urlManager.toSource();
2062     e.visitors = this.visitors.toSource();
2063
2064     if (!this.resumable && !this.is(COMPLETE)) {
2065       e.totalSize = 0;
2066     }
2067     else {
2068       e.totalSize = this.totalSize;
2069     }
2070    
2071     e.chunks = [];
2072
2073     if (this.isOf(RUNNING, PAUSED, QUEUED) && this.resumable) {
2074       for each (let c in this.chunks) {
2075         e.chunks.push({start: c.start, end: c.end, written: c.safeBytes});
2076       }
2077     }
2078     return JSONCompat.stringify(e);
2079   }
2080 }
2081
2082 function Chunk(download, start, end, written) {
2083   // saveguard against null or strings and such
2084   this._written = written > 0 ? written : 0;
2085   this._buffered = 0;
2086   this._start = start;
2087   this._end = end;
2088   this.end = end;
2089   this._parent = download;
2090   this._sessionbytes = 0;
2091 }
2092
2093 Chunk.prototype = {
2094   running: false,
2095   get starter() {
2096     return this.end <= 0;
2097   },
2098   get start() {
2099     return this._start;
2100   },
2101   get end() {
2102     return this._end;
2103   },
2104   set end(nv) {
2105     this._end = nv;
2106     this._total = this._end - this._start + 1;
2107   },
2108   get total() {
2109     return this._total;
2110   },
2111   get written() {
2112     return this._written;
2113   },
2114   get safeBytes() {
2115     return this.written - this._buffered;
2116   },
2117   get remainder() {
2118     return this._total - this._written;
2119   },
2120   get complete() {
2121     if (this._end == -1) {
2122       return this.written != 0;
2123     }
2124     return this._total == this.written;
2125   },
2126   get parent() {
2127     return this._parent;
2128   },
2129   merge: function CH_merge(ch) {
2130     if (!this.complete && !ch.complete) {
2131       throw new Error("Cannot merge incomplete chunks this way!");
2132     }
2133     this.end = ch.end;
2134     this._written += ch._written;
2135   },
2136   open: function CH_open() {
2137     this._sessionBytes = 0;
2138     let file = this.parent.tmpFile;
2139     if (!file.parent.exists()) {
2140       file.parent.create(Ci.nsIFile.DIRECTORY_TYPE, Prefs.dirPermissions);
2141     }   
2142     let outStream = new FileOutputStream(file, 0x02 | 0x08, Prefs.permissions, 0);
2143     let seekable = outStream.QueryInterface(Ci.nsISeekableStream);
2144     seekable.seek(0x00, this.start + this.written);
2145     this._outStream = new BufferedOutputStream(outStream, CHUNK_BUFFER_SIZE);
2146    
2147     this.buckets = new ByteBucketTee(
2148         this.parent.bucket,
2149         Limits.getServerBucket(this.parent),
2150         GlobalBucket
2151         );
2152     this.buckets.register(this);
2153   },
2154   close: function CH_close() {
2155     this.running = false;
2156     if (this._outStream) {
2157       this._outStream.flush();
2158       this._outStream.close();
2159       delete this._outStream;
2160     }
2161     this._buffered = 0;
2162     if (this.parent.is(CANCELED)) {
2163       this.parent.removeTmpFile();
2164     }
2165     if (this.buckets) {
2166       this.buckets.unregister(this);
2167     }
2168     delete this._req;
2169     this._sessionBytes = 0;
2170   },
2171   rollback: function CH_rollback() {
2172     if (!this._sessionBytes || this._sessionBytes > this._written) {
2173       return;
2174     }
2175     this._written -= this._sessionBytes;
2176     this._sessionBytes = 0;
2177   },
2178   cancel: function CH_cancel() {
2179     this.running = false;
2180     this.close();
2181     if (this.download) {
2182       this.download.cancel();
2183     }
2184   },
2185   _wnd: 0,
2186   _written: 0,
2187   _outStream: null,
2188   write: function CH_write(aRequest, aInputStream, aCount) {
2189     try {
2190       // not running: do not write anything
2191       if (!this.running) {
2192         return 0;
2193       }
2194       if (!this._outStream) {
2195         this.open();
2196         this._wnd = 1024;
2197       }
2198       let bytes = this.remainder;
2199       if (!this.total || aCount < bytes) {
2200         bytes = aCount;
2201       }
2202       if (!bytes) {
2203         // we got what we wanted
2204         return -1;
2205       }
2206       bytes = Math.min(Math.round(this._wnd), bytes);
2207       let got = this.buckets.requestBytes(bytes);
2208       if (got < bytes) {
2209         this._wnd = Math.max(this._wnd * 0.5, 512);
2210         this._req = aRequest;
2211         this._req.suspend();
2212       }
2213       else {
2214         this._wnd += 256;
2215       }
2216       bytes = got;
2217       if (!bytes) {
2218         return bytes;
2219       }
2220       if (bytes < 0) {
2221         throw new Exception("bytes negative");
2222       }
2223       // we're using nsIFileOutputStream
2224       if (this._outStream.writeFrom(aInputStream, bytes) != bytes) {
2225         throw ("chunks::write: read/write count mismatch!");
2226       }
2227       this._written += bytes;
2228       this._sessionBytes += bytes;
2229       this._buffered = Math.min(CHUNK_BUFFER_SIZE, this._buffered + bytes);
2230
2231       this.parent.timeLastProgress = Utils.getTimestamp();
2232
2233       return bytes;
2234     }
2235     catch (ex) {
2236       Debug.log('write: ' + this.parent.tmpFile.path, ex);
2237       throw ex;
2238     }
2239     return 0;
2240   },
2241   observe: function() {
2242     if (!this._req) {
2243       return;
2244     }
2245     let req = this._req;
2246     delete this._req;
2247     req.resume();
2248   },
2249   toString: function() {
2250     let len = this.parent.totalSize ? String(this.parent.totalSize).length  : 10;
2251     return Utils.formatNumber(this.start, len)
2252       + "/"
2253       + Utils.formatNumber(this.end, len)
2254       + "/"
2255       + Utils.formatNumber(this.total, len)
2256       + " running:"
2257       + this.running
2258       + " written/remain:"
2259       + Utils.formatNumber(this.written, len)
2260       + "/"
2261       + Utils.formatNumber(this.remainder, len);
2262   }
2263 }
2264
2265 function Connection(d, c, isInfoGetter) {
2266
2267   this.d = d;
2268   this.c = c;
2269   this.isInfoGetter = isInfoGetter;
2270   this.url = d.urlManager.getURL();
2271   let referrer = d.referrer;
2272   Debug.logString("starting: " + this.url.url.spec);
2273
2274   this._chan = IOService.newChannelFromURI(this.url.url);
2275   let r = Ci.nsIRequest;
2276   let loadFlags = r.LOAD_NORMAL
2277   if (!Preferences.getExt('useCache', false)) {
2278     loadFlags = loadFlags | r.LOAD_BYPASS_CACHE;
2279   }
2280   else {
2281     Debug.logString("using cache");
2282   }
2283   this._chan.loadFlags = loadFlags;
2284   this._chan.notificationCallbacks = this;
2285   try {
2286     let encodedChannel = this._chan.QueryInterface(Ci.nsIEncodedChannel);
2287     encodedChannel.applyConversion = false;
2288   }
2289   catch (ex) {
2290     // no-op
2291   }
2292   if (this._chan instanceof Ci.nsIHttpChannel) {
2293     try {
2294       Debug.logString("http");
2295       let http = this._chan.QueryInterface(Ci.nsIHttpChannel);
2296       if (c.start + c.written > 0) {
2297         http.setRequestHeader('Range', 'bytes=' + (c.start + c.written) + "-", false);
2298       }
2299       if (this.isInfoGetter) {
2300         if (!d.fromMetalink) {
2301           http.setRequestHeader('Accept', 'application/metalink+xml;q=0.9', true);
2302         }
2303         http.setRequestHeader('Want-Digest', DTA.WANT_DIGEST_STRING, false);
2304       }
2305       if (referrer instanceof Ci.nsIURI) {
2306         http.referrer = referrer;
2307       }
2308       if (Prefs.noKeepAlive) {
2309         http.setRequestHeader('Keep-Alive', '', false);
2310         http.setRequestHeader('Connection', 'close', false);
2311       }
2312       if (d.postData) {
2313         let uc = http.QueryInterface(Ci.nsIUploadChannel);
2314         uc.setUploadStream(new StringInputStream(d.postData, d.postData.length), null, -1);
2315         http.requestMethod = 'POST';
2316       }     
2317     }
2318     catch (ex) {
2319       Debug.log("error setting up http channel", ex);
2320       // no-op
2321     }
2322   }
2323   else if (this._chan instanceof Ci.nsIFTPChannel) {
2324     try {
2325       let ftp = this._chan.QueryInterface(Ci.nsIFTPChannel);
2326       if (c.start + c.written > 0) {
2327           let resumable = ftp.QueryInterface(Ci.nsIResumableChannel);
2328           resumable.resumeAt(c.start + c.written, '');
2329       }       
2330     }
2331     catch (ex) {
2332       Debug.log('error setting up ftp channel', ex);
2333     }
2334   }
2335   try {
2336     let prio = this._chan.QueryInterface(Ci.nsISupportsPriority);
2337     prio.adjustPriority(Ci.nsISupportsPriority.PRIORITY_LOW);
2338   }
2339   catch (ex) {
2340     Debug.log("Failed setting priority", ex);
2341   }
2342   this.c.running = true;
2343   this._chan.asyncOpen(this, null);
2344 }
2345
2346 Connection.prototype = {
2347   _interfaces: [
2348     Ci.nsISupports,
2349     Ci.nsISupportsWeakReference,
2350     Ci.nsIWeakReference,
2351     Ci.nsICancelable,
2352     Ci.nsIInterfaceRequestor,
2353     Ci.nsIStreamListener,
2354     Ci.nsIRequestObserver,
2355     Ci.nsIProgressEventSink,
2356     Ci.nsIChannelEventSink,
2357     Ci.nsIFTPEventSink,
2358   ],
2359  
2360   cantCount: false,
2361
2362   QueryInterface: function DL_QI(iid) {
2363     if (this._interfaces.some(function(i) { return iid.equals(i); })) {
2364       return this;
2365     }
2366     Debug.log("Interface not implemented " + iid, Components.results.NS_ERROR_NO_INTERFACE);
2367     throw Components.results.NS_ERROR_NO_INTERFACE;
2368   },
2369   // nsISupportsWeakReference
2370   GetWeakReference: function DL_GWR() {
2371     return this;
2372   },
2373   // nsIWeakReference
2374   QueryReferent: function DL_QR(uuid) {
2375     return this.QueryInterface(uuid);
2376   },
2377   // nsICancelable
2378   cancel: function DL_cancel(aReason) {
2379     try {
2380       if (this._closed) {
2381         return;
2382       }
2383       Debug.logString("cancel");
2384       if (!aReason) {
2385         aReason = NS_ERROR_BINDING_ABORTED;
2386       }
2387       this._chan.cancel(aReason);
2388       this._closed = true;
2389     }
2390     catch (ex) {
2391       Debug.log("cancel", ex);
2392     }
2393   },
2394   // nsIInterfaceRequestor
2395   _notImplemented: [
2396     Ci.nsIDocShellTreeItem, // cookie same-origin checks
2397     Ci.nsIDOMWindow, // cookie same-origin checks
2398     Ci.nsIWebProgress,
2399   ],
2400   getInterface: function DL_getInterface(iid) {
2401     if (this._notImplemented.some(function(i) { return iid.equals(i); })) {
2402       // we don't want to implement these
2403       // and we don't want them to pop up in our logs
2404       throw Components.results.NS_ERROR_NO_INTERFACE;
2405     }
2406     if (iid.equals(Ci.nsIAuthPrompt)) {
2407       return AuthPrompts.authPrompter;
2408     }
2409     if (iid.equals(Ci.nsIPrompt)) {
2410       return AuthPrompts.prompter;
2411     }
2412     if ('nsIAuthPrompt2' in Ci && iid.equals(Ci.nsIAuthPrompt2)) {
2413       return AuthPrompts.authPrompter.QueryInterface(Ci.nsIAuthPrompt2);
2414     }
2415     try {
2416       return this.QueryInterface(iid);
2417     }
2418     catch (ex) {
2419       Debug.log("interface not implemented: " + iid, ex);
2420       throw ex;
2421     }
2422   },
2423
2424   // nsIChannelEventSink
2425   onChannelRedirect: function DL_onChannelRedirect(oldChannel, newChannel, flags) {
2426     let c = this.c;
2427     try {
2428       if (!(oldChannel instanceof Ci.nsIChannel) || !(newChannel instanceof Ci.nsIChannel)) {
2429         throw new Exception("redirect: requests not channels");
2430       }
2431      
2432       // When we get redirected from, say, http to ftp, we need to explicitly
2433       // call resumeAt() as this won't be propagated from the old channel.
2434       if (c.start + c.written > 0 && !(newChannel instanceof Ci.nsIHttpChannel)) {
2435         let resumable = newChannel.QueryInterface(Ci.nsIResumableChannel);
2436         resumable.resumeAt(c.start + c.written, '');
2437         Debug.logString("redirect: set resumeAt on " + newChannel.URI.spec + "/" + newChannel.originalURI.spec + " at " + (c.start + c.written));
2438       }
2439     }
2440     catch (ex) {
2441       Debug.log("redirect: cannot resumeAt", ex);
2442       if (!this.handleError()) {
2443         d.fail(_('servererror'), _('ftperrortext'), _('servererror'));
2444         return;
2445       }
2446     }
2447      
2448     this._chan = newChannel;
2449    
2450     if (!this.isInfoGetter) {
2451       return;
2452     }
2453     try {
2454       let newurl = new DTA.URL(newChannel.URI.QueryInterface(Ci.nsIURL), this.url.preference);
2455       this.d.urlManager.replace(this.url, newurl);
2456       this.url = newurl;
2457       this.d.fileName = this.url.usable.getUsableFileName();
2458     }
2459     catch (ex) {
2460       Debug.log("Failed to reset data on channel redirect", ex);
2461     }
2462   },
2463  
2464   // nsIStreamListener
2465   onDataAvailable: function DL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) {
2466     if (this._closed) {
2467       throw 0x804b0002; // NS_BINDING_ABORTED;
2468     }
2469     try {
2470       // we want to kill ftp chans as well which do not seem to respond to
2471       // cancel correctly.
2472       if (0 > this.c.write(aRequest, aInputStream, aCount)) {
2473         // we already got what we wanted
2474         this.cancel();
2475       }
2476     }
2477     catch (ex) {
2478       Debug.log('onDataAvailable', ex);
2479       this.d.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
2480     }
2481   },
2482  
2483   // nsIFTPEventSink
2484   OnFTPControlLog: function(server, msg) {
2485     /*
2486      * Very hacky :p If we don't handle it here, then nsIFTPChannel will + try
2487      * to CWD to the file (d'oh) + afterwards ALERT (modally) that the CWD
2488      * didn't succeed (double-d'oh)
2489      */
2490     if (!server) {
2491       this._wasRetr = /^RETR/.test(msg) || /^REST/.test(msg);
2492     }
2493   },
2494  
2495   handleError: function DL_handleError() {
2496     let c = this.c;
2497     let d = this.d;
2498    
2499     c.cancel();
2500     d.dumpScoreboard();
2501     if (d.chunks.indexOf(c) == -1) {
2502       // already killed;
2503       return true;
2504     }
2505
2506     Debug.logString("handleError: problem found; trying to recover");
2507    
2508     if (d.urlManager.markBad(this.url)) {
2509       Debug.logString("handleError: fresh urls available, kill this one and use another!");
2510       d.timeLastProgress = Utils.getTimestamp();
2511       return true;
2512     }
2513    
2514     Debug.logString("affected: " + c);
2515     d.dumpScoreboard();
2516    
2517     let max = -1, found = null;
2518     for each (let cmp in d.chunks) {
2519       if (!cmp.running) {
2520         continue;
2521       }
2522       if (cmp.start < c.start && cmp.start > max) {
2523         found = cmp;
2524         max = cmp.start;
2525       }
2526     }
2527     if (found) {
2528       Debug.logString("handleError: found joinable chunk; recovering suceeded, chunk: " + found);
2529       found.end = c.end;
2530       if (--d.maxChunks == 1) {
2531         // d.resumable = false;
2532       }
2533       d.chunks = d.chunks.filter(function(ch) ch != c);
2534       d.chunks.sort(function(a, b) a.start - b.start);
2535      
2536       // check for overlapping ranges we might have created
2537       // otherwise we'll receive a size mismatch
2538       // this means that we're gonna redownload an already finished chunk...
2539       for (let i = d.chunks.length - 2; i > -1; --i) {
2540         let c1 = d.chunks[i], c2 = d.chunks[i + 1];
2541         if (c1.end >= c2.end) {
2542           if (c2.running) {
2543             // should never ever happen :p
2544             d.dumpScoreboard();
2545             Debug.logString("overlapping:\n" + c1 + "\n" + c2);
2546             d.fail("Internal error", "Please notify the developers that there were 'overlapping chunks'!", "Internal error (please report)");
2547             return false;
2548           }
2549           d.chunks.splice(i + 1, 1);
2550         }
2551       }
2552       let ac = 0;
2553       d.chunks.forEach(function(c) { if (c.running) { ++ac; }});
2554       d.activeChunks = ac;
2555       c.close();
2556      
2557       d.save();
2558       d.dumpScoreboard();
2559       return true;
2560     }
2561     Debug.logString("recovery failed");
2562     return false;
2563   },
2564   handleHttp: function DL_handleHttp(aChannel) {
2565     let c = this.c;
2566     let d = this.d;
2567    
2568     let code = 0, status = 'Server returned nothing';
2569     try {
2570       code = aChannel.responseStatus;
2571       status = aChannel.responseStatusText;
2572     }
2573     catch (ex) {
2574       return true;
2575     }
2576      
2577     if (code >= 400) {
2578       // any data that we got over this channel should be considered "corrupt"
2579       c.rollback();
2580      
2581       if (c.starter && d.urlManager.markBad(this.url)) {
2582         Debug.log("caught bad server (Error: " + code + ")", d.toString());
2583         d.cancel();
2584         d.safeRetry();
2585         return false;
2586       }
2587       if (!this.handleError()) {
2588         Debug.log("handleError: Cannot recover from problem!", code);
2589         if ([401, 402, 407, 500, 502, 503, 504].indexOf(code) != -1 || Prefs.recoverAllHttpErrors) {
2590           Debug.log("we got temp failure!", code);
2591           Dialog.markAutoRetry(d);
2592           d.pause();
2593           d.status = code >= 500 ? _('temperror') : _('autherror');
2594         }
2595         else if (code == 450) {
2596           d.fail(
2597             _('pcerrortitle'),
2598             _('pcerrortext'),
2599             _('pcerrortitle')
2600           );
2601         }
2602         else {
2603           var file = d.fileName.length > 50 ? d.fileName.substring(0, 50) + "..." : d.fileName;
2604           code = Utils.formatNumber(code, 3);
2605           if (Prefs.resumeOnError) {
2606             Dialog.markAutoRetry(d);
2607             d.pause();
2608             d.status = _('temperror');
2609           }
2610           else {
2611             d.fail(
2612               _("error", [code]),
2613               _("failed", [file]) + " " + _("sra", [code]) + ": " + status,
2614               _("error", [code])
2615             );
2616           }
2617         }
2618         d.save();
2619       }
2620       return false;
2621     }
2622
2623     // not partial content altough we are multi-chunk
2624     if (code != 206 && !this.isInfoGetter) {
2625       Debug.log(d + ": Server returned a " + aChannel.responseStatus + " response instead of 206", this.isInfoGetter);
2626      
2627       if (!this.handleError()) {
2628         vis = {value: '', visitHeader: function(a,b) { this.value += a + ': ' + b + "\n"; }};
2629         aChannel.visitRequestHeaders(vis);
2630         Debug.logString("Request Headers\n\n" + vis.value);
2631         vis.value = '';
2632         aChannel.visitResponseHeaders(vis);
2633         Debug.logString("Response Headers\n\n" + vis.value);
2634         d.cancel();
2635         d.resumable = false;
2636         d.safeRetry();
2637         return false;
2638       }
2639     }
2640
2641     var visitor = null;
2642     try {
2643       visitor = d.visitors.visit(aChannel);
2644     }
2645     catch (ex) {
2646       Debug.log("header failed! " + d, ex);
2647       // restart download from the beginning
2648       d.cancel();
2649       d.resumable = false;
2650       d.safeRetry();
2651       return false;
2652     }
2653    
2654     if (!this.isInfoGetter) {
2655       return false;
2656     }
2657
2658     if (visitor.type) {
2659       d.contentType = visitor.type;
2660     }
2661
2662     // compression?
2663     if (['gzip', 'deflate'].indexOf(visitor.encoding) != -1 && !d.contentType.match(/gzip/i) && !d.fileName.match(/\.gz$/i)) {
2664       d.compression = visitor.encoding;
2665     }
2666     else {
2667       d.compression = null;
2668     }
2669    
2670     if (visitor.hash && (!d.hashCollection || !d.hashCollection.full || d.hashCollection.full.q < visitor.hash.q)) {
2671       d.hashCollection = new DTA.HashCollection(visitor.hash);
2672     }
2673
2674     // accept range
2675     d.resumable &= visitor.acceptRanges;
2676
2677     if (visitor.type && visitor.type.search(/application\/metalink\+xml/) != -1) {
2678       d.isMetalink = true;
2679       d.resumable = false;
2680     }
2681
2682     if (visitor.contentLength > 0) {
2683       d.totalSize = visitor.contentLength;
2684     }
2685     else {
2686       d.totalSize = 0;
2687     }
2688    
2689     if (visitor.fileName && visitor.fileName.length > 0) {
2690       // if content disposition hasn't an extension we use extension of URL
2691       let newName = visitor.fileName.getUsableFileName();
2692       let ext = this.url.usable.getExtension();
2693       if (visitor.fileName.lastIndexOf('.') == -1 && ext) {
2694         newName += '.' + ext;
2695       }
2696       d.fileName = newName.getUsableFileName();
2697     }
2698
2699     return false;
2700   },
2701  
2702   // Generic handler for now :p
2703   handleFtp: function  DL_handleFtp(aChannel) {
2704     let c = this.c;
2705     let d = this.d;
2706     try {
2707       let pb = aChannel.QueryInterface(Ci.nsIPropertyBag2);
2708       let totalSize = Math.max(pb.getPropertyAsInt64('content-length'), 0);
2709       if (d.totalSize && totalSize != this.totalSize && !this.handleError()) {
2710         Debug.logString("ftp: total size mismatch " + totalSize + " " + this.totalSize);
2711         d.fail(_('servererror'), _('ftperrortext'), _('servererror'));
2712         return false;
2713       }
2714       Debug.logString("ftp: total size is: " + totalSize + " for: " + this.url);
2715       d.totalSize = totalSize;
2716     }
2717     catch (ex) {
2718       Debug.log("ftp: no totalsize", ex);
2719       if (c.start != 0 && !this.handleError()) {
2720         d.fail(_('servererror'), _('ftperrortext'), _('servererror'));
2721         return false;
2722       }
2723       d.totalSize = 0;
2724       d.resumable = false;
2725     }
2726    
2727     try {
2728       aChannel.QueryInterface(Ci.nsIResumableChannel).entityID;
2729     }
2730     catch (ex) {
2731       Debug.logString("likely not resumable or connection refused!");
2732       if (!this.handleError()) {
2733         // restart download from the beginning
2734         d.fail(_('servererror'), _('ftperrortext'), _('servererror'));
2735         return false;
2736       }
2737     }
2738    
2739     try {
2740       let visitor = d.visitors.visit(aChannel.QueryInterface(Ci.nsIChannel));
2741     }
2742     catch (ex) {
2743       Debug.log("header failed! " + d, ex);
2744       // restart download from the beginning
2745       d.cancel();
2746       d.resumable = false;
2747       d.safeRetry();
2748       return false;
2749     }
2750     return false;
2751   },
2752  
2753   handleGeneric: function DL_handleGeneric(aChannel) {
2754     var c = this.c;
2755     var d = this.d;
2756    
2757     // hack: determine if we are a multi-part chunk,
2758     // if so something bad happened, 'cause we aren't supposed to be multi-part
2759     if (c.start != 0 && d.is(RUNNING)) {
2760       if (!this.handleError()) {
2761         Debug.log(d + ": Server error or disconnection", "(type 1)");
2762         Dialog.markAutoRetry(d);
2763         d.status = _("servererror");
2764         d.pause();
2765       }
2766       return false;
2767     }     
2768      
2769     // try to get the size anyway ;)
2770     try {
2771       let pb = aChannel.QueryInterface(Ci.nsIPropertyBag2);
2772       d.totalSize = Math.max(pb.getPropertyAsInt64('content-length'), 0);
2773     }
2774     catch (ex) {
2775       try {
2776         d.totalSize = Math.max(aChannel.contentLength, 0);
2777       }
2778       catch (ex) {
2779         d.totalSize = 0;
2780       }
2781     }
2782     d.resumable = false;
2783     return false;
2784   },
2785  
2786   // nsIRequestObserver,
2787   _supportedChannels: [
2788     {i:Ci.nsIHttpChannel, f:'handleHttp'},
2789     {i:Ci.nsIFTPChannel, f:'handleFtp'},
2790     {i:Ci.nsIChannel, f:'handleGeneric'}
2791   ],
2792   onStartRequest: function DL_onStartRequest(aRequest, aContext) {
2793     let c = this.c;
2794     let d = this.d;
2795     Debug.logString('StartRequest: ' + c);
2796  
2797     this.started = true;
2798     try {
2799       for each (let sc in this._supportedChannels) {
2800         let chan = null;
2801         try {
2802           chan = aRequest.QueryInterface(sc.i);
2803           if ((this.rexamine = this[sc.f](chan))) {
2804              return;
2805           }
2806           break;
2807         }
2808         catch (ex) {
2809           // continue
2810         }
2811       }
2812
2813       if (this.isInfoGetter) {
2814         if (d.fileName.getExtension() == 'metalink') {
2815           d.isMetalink = true;
2816           d.resumable = false;
2817         }       
2818        
2819         // Checks for available disk space.
2820         var tsd = d.totalSize;
2821         try {
2822           if (tsd) {
2823             let tmp = Prefs.tempLocation, vtmp = 0;
2824             if (tmp) {
2825               vtmp = Utils.validateDir(tmp);
2826               if (!vtmp && Utils.getFreeDisk(vtmp) < tsd) {
2827                 d.fail(_("ndsa"), _("spacetemp"), _("freespace"));
2828                 return;
2829               }
2830             }
2831             let realDest = Utils.validateDir(d.destinationPath);
2832             if (!realDest) {
2833               throw new Error("invalid destination folder");
2834             }
2835             var nsd = Utils.getFreeDisk(realDest);
2836             // Same save path or same disk (we assume that tmp.avail ==
2837             // dst.avail means same disk)
2838             // simply moving should succeed
2839             if (d.compression && (!tmp || Utils.getFreeDisk(vtmp) == nsd)) {
2840               // we cannot know how much space we will consume after
2841               // decompressing.
2842               // so we assume factor 1.0 for the compressed and factor 1.5 for
2843               // the decompressed file.
2844               tsd *= 2.5;
2845             }
2846             if (nsd < tsd) {
2847               Debug.logString("nsd: " +  nsd + ", tsd: " + tsd);
2848               d.fail(_("ndsa"), _("spacedir"), _("freespace"));
2849               return;
2850             }
2851           }
2852         }
2853         catch (ex) {
2854           Debug.log("size check threw", ex);
2855           d.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
2856           return;
2857         }
2858        
2859         if (!d.totalSize) {
2860           d.resumable = false;         
2861           this.cantCount = true;
2862         }
2863        
2864         if (!d.resumable) {
2865           d.maxChunks = 1;
2866         }
2867         c.end = d.totalSize - 1;
2868         delete this.isInfoGetter;
2869        
2870         // Explicitly trigger rebuildDestination here, as we might have received
2871         // a html content type and need to rewrite the file
2872         d.rebuildDestination();
2873         ConflictManager.resolve(d);
2874       }
2875      
2876       if (d.resumable && !d.is(CANCELED)) {
2877         d.resumeDownload();
2878       }
2879     }
2880     catch (ex) {
2881       Debug.log("onStartRequest", ex);
2882     }
2883   },
2884   onStopRequest: function DL_onStopRequest(aRequest, aContext, aStatusCode) {
2885     try {
2886       Debug.logString('StopRequest');
2887     }
2888     catch (ex) {
2889       return;
2890     }
2891    
2892     // shortcuts
2893     let c = this.c;
2894     let d = this.d;
2895     c.close();
2896    
2897     if (d.chunks.indexOf(c) == -1) {
2898       return;
2899     }
2900
2901     // update flags and counters
2902     d.refreshPartialSize();
2903     --d.activeChunks;
2904
2905     // check if we're complete now
2906     if (d.is(RUNNING) && d.chunks.every(function(e) { return e.complete; })) {
2907       if (!d.resumeDownload()) {
2908         d.state = FINISHING;
2909         Debug.logString(d + ": Download is complete!");
2910         d.finishDownload();
2911         return;
2912       }
2913     }
2914    
2915     if (c.starter && -1 != [
2916       NS_ERROR_CONNECTION_REFUSED,
2917       NS_ERROR_UNKNOWN_HOST,
2918       NS_ERROR_NET_TIMEOUT,
2919       NS_ERROR_NET_RESET
2920     ].indexOf(aStatusCode)) {
2921       if (!d.urlManager.markBad(this.url)) {
2922         Debug.log(d + ": Server error or disconnection", "(type 3)");
2923         Dialog.markAutoRetry(d);
2924         d.pause();
2925         d.status = _("servererror");
2926       }
2927       else {
2928         Debug.log("caught bad server", d.toString());
2929         d.cancel();
2930         d.safeRetry();
2931       }
2932       return;     
2933     }
2934    
2935     // work-around for ftp crap
2936     // nsiftpchan for some reason assumes that if RETR fails it is a directory
2937     // and tries to advance into said directory
2938     if (aStatusCode == NS_ERROR_FTP_CWD) {
2939       Debug.logString("Cannot change to directory :p", aStatusCode);
2940       if (!this.handleError()) {
2941         d.fail(_('servererror'), _('ftperrortext'), _('servererror'));
2942       }
2943       return;
2944     }
2945      
2946     // routine for normal chunk
2947     Debug.logString(this.url + ": Chunk " + c.start + "-" + c.end + " finished.");
2948    
2949     // rude way to determine disconnection: if connection is closed before
2950     // download is started we assume a server error/disconnection
2951     if (c.starter && d.is(RUNNING)) {
2952       if (!d.urlManager.markBad(this.url)) {
2953         Debug.log(d + ": Server error or disconnection", "(type 2)");
2954         Dialog.markAutoRetry(d);
2955         d.pause();
2956         d.status = _("servererror");
2957       }
2958       else {
2959         Debug.log("caught bad server", d.toString());
2960         d.cancel();
2961         d.safeRetry();
2962       }
2963       return;     
2964     }
2965    
2966     // Server did not return any data.
2967     // Try to mark the URL bad
2968     // else pause + autoretry
2969     if (!c.written  && !!c.remainder) {
2970       if (!d.urlManager.markBad(this.url)) {
2971         Debug.log(d + ": Server error or disconnection", "(type 1)");
2972         Dialog.markAutoRetry(d);
2973         d.pause();
2974         d.status = _("servererror");
2975       }
2976       return;
2977     }
2978
2979     if (!d.isOf(PAUSED, CANCELED, FINISHING) && d.chunks.length == 1 && d.chunks[0] == c) {
2980       if (d.resumable || Prefs.resumeOnError) {
2981         Dialog.markAutoRetry(d);
2982         d.pause();
2983         d.status = _('errmismatchtitle');
2984       }
2985       else {
2986         d.fail(
2987           _('errmismatchtitle'),
2988           _('errmismatchtext', [d.partialSize, d.totalSize]),
2989           _('errmismatchtitle')
2990         );
2991       }
2992       return;     
2993     }
2994     if (!d.isOf(PAUSED, CANCELED)) {
2995       d.resumeDownload();
2996     }
2997   },
2998
2999   // nsIProgressEventSink
3000   onProgress: function DL_onProgress(aRequest, aContext, aProgress, aProgressMax) {
3001     try {
3002       // shortcuts
3003       let c = this.c;
3004       let d = this.d;
3005      
3006       if (this.reexamine) {
3007         Debug.logString(d + ": reexamine");
3008         this.onStartRequest(aRequest, aContext);
3009         if (this.reexamine) {
3010           return;
3011         }
3012       }
3013
3014       // update download tree row
3015       if (d.is(RUNNING)) {
3016         d.refreshPartialSize();
3017
3018         if (!this.resumable && d.totalSize) {
3019           // basic integrity check
3020           if (d.partialSize > d.totalSize) {
3021             d.dumpScoreboard();
3022             Debug.logString(d + ": partialSize > totalSize" + "(" + d.partialSize + "/" + d.totalSize + "/" + ( d.partialSize - d.totalSize) + ")");
3023             d.fail(
3024               _('errmismatchtitle'),
3025               _('errmismatchtext', [d.partialSize, d.totalSize]),
3026               _('errmismatchtitle')
3027             );
3028             return;
3029           }
3030         }
3031         else {
3032           d.status = _("downloading");
3033         }
3034       }
3035     }
3036     catch(ex) {
3037       Debug.log("onProgressChange():", e);
3038     }
3039   },
3040   onStatus: function  DL_onStatus(aRequest, aContext, aStatus, aStatusArg) {}
3041 };
3042
3043 function startDownloads(start, downloads) {
3044
3045   var numbefore = Tree.rowCount - 1;
3046  
3047   let g = downloads;
3048   if ('length' in downloads) {
3049     g = (i for each (i in downloads));
3050   }
3051
3052   let added = 0;
3053   let removeableTabs = {};
3054   Tree.beginUpdate();
3055   QueueStore.beginUpdate();
3056   for (let e in g) {
3057     let qi = new QueueItem();
3058     let lnk = e.url;
3059     if (typeof lnk == 'string') {
3060       qi.urlManager = new UrlManager([new DTA.URL(IOService.newURI(lnk, null, null))]);
3061     }
3062     else if (lnk instanceof UrlManager) {
3063       qi.urlManager = lnk;
3064     }
3065     else {
3066       qi.urlManager = new UrlManager([lnk]);
3067     }
3068     qi.numIstance = e.numIstance;
3069  
3070     if (e.referrer) {
3071       try {
3072         qi.referrer = e.referrer.toURL();
3073       }
3074       catch (ex) {
3075         // We might have been fed with about:blank or other crap. so ignore.
3076       }
3077     }
3078     // only access the setter of the last so that we don't generate stuff trice.
3079     qi._pathName = e.dirSave.addFinalSlash().toString();
3080     qi._description = !!e.description ? e.description : '';
3081     qi._title = !!e.title ? e.title : '';
3082     qi._mask = e.mask;
3083     qi.fromMetalink = !!e.fromMetalink;
3084     qi.fileName = qi.urlManager.usable.getUsableFileName();
3085     if (e.fileName) {
3086       qi.fileName = e.fileName.getUsableFileName();
3087     }
3088     if (e.destinationName) {
3089       qi.destinationName = e.destinationName.getUsableFileName();
3090     }
3091     if (e.startDate) {
3092       qi.startDate = e.startDate;
3093     }
3094    
3095     // hash?
3096     if (e.hashCollection) {
3097       qi.hashCollection = e.hashCollection;
3098     }
3099     else if (e.url.hashCollection) {
3100       qi.hashCollection = e.url.hashCollection;
3101     }
3102     else if (e.hash) {
3103       qi.hashCollection = new DTA.HashCollection(e.hash);
3104     }
3105     else if (e.url.hash) {
3106       qi.hashCollection = new DTA.HashCollection(e.url.hash);
3107     }
3108     else {
3109       qi.hashCollection = null; // to initialize prettyHash
3110     }
3111
3112     let postData = ContentHandling.getPostDataFor(qi.urlManager.url);
3113     if (e.url.postData) {
3114       postData = e.url.postData;
3115     }
3116     if (postData) {
3117       qi.postData = postData;
3118     }   
3119
3120     qi.state = start ? QUEUED : PAUSED;
3121     if (qi.is(QUEUED)) {
3122       qi.status = TEXT_QUEUED;
3123     }
3124     else {
3125       qi.status = TEXT_PAUSED;
3126     }
3127     qi._position = Tree.add(qi);
3128     qi.save();   
3129     ++added;
3130   }
3131   QueueStore.endUpdate();
3132   Tree.endUpdate();
3133
3134   var boxobject = Tree._box;
3135   boxobject.QueryInterface(Ci.nsITreeBoxObject);
3136   if (added <= boxobject.getPageLength()) {
3137     boxobject.scrollToRow(Tree.rowCount - boxobject.getPageLength());
3138   }
3139   else {
3140     boxobject.scrollToRow(numbefore);
3141   }
3142 }
3143
3144 var ConflictManager = {
3145   _items: [],
3146   resolve: function CM_resolve(download, reentry) {
3147     if (!this._check(download)) {
3148       if (reentry) {
3149         download[reentry]();
3150       }
3151       return;
3152     }
3153     for each (let item in this._items.length) {
3154       if (item.download == download) {
3155         Debug.logString("conflict resolution updated to: " + reentry);
3156         item.reentry = reentry;
3157         return;
3158       }
3159     }
3160     Debug.logString("conflict resolution queued to: " + reentry);
3161     this._items.push({download: download, reentry: reentry});
3162     this._process();
3163   },
3164   _check: function CM__check(download) {
3165     let dest = new FileFactory(download.destinationFile);
3166     let sn = false;
3167     if (download.is(RUNNING)) {
3168       sn = Dialog.checkSameName(download, download.destinationFile);
3169     }
3170     Debug.logString("conflict check: " + sn + "/" + dest.exists() + " for " + download.destinationFile);
3171     return dest.exists() || sn;
3172   },
3173   _process: function CM__process() {
3174     if (this._processing) {
3175       return;
3176     }
3177     let cur;
3178     while (this._items.length) {
3179       cur = this._items[0];
3180       if (!this._check(cur.download)) {
3181         if (cur.reentry) {
3182           cur.download[cur.reentry]();
3183         }
3184         this._items.shift();
3185         continue;
3186       }
3187       break;
3188     }
3189     if (!this._items.length) {
3190       return;
3191     }
3192  
3193     if (Prefs.conflictResolution != 3) {
3194       this._return(Prefs.conflictResolution);
3195       return;
3196     }
3197     if ('_sessionSetting' in this) {
3198       this._return(this._sessionSetting);
3199       return;
3200     }
3201     if (cur.download.shouldOverwrite) {
3202       this._return(1);
3203       return;
3204     }
3205    
3206     this._computeConflicts(cur);
3207
3208     var options = {
3209       url: cur.download.urlManager.usable.cropCenter(45),
3210       fn: cur.download.destinationName.cropCenter(45),
3211       newDest: cur.newDest.cropCenter(45)
3212     };
3213    
3214     this._processing = true;
3215    
3216     window.openDialog(
3217       "chrome://dta/content/dta/manager/conflicts.xul",
3218       "_blank",
3219       "chrome,centerscreen,resizable=no,dialog,close=no,dependent",
3220       options, this
3221     );
3222   },
3223   _computeConflicts: function CM__computeConflicts(cur) {
3224     let download = cur.download;
3225     download.conflicts = 0;
3226     let basename = download.destinationName;
3227     let newDest = new FileFactory(download.destinationFile);
3228     let i = 1;
3229     for (;; ++i) {
3230       newDest.leafName = Utils.formatConflictName(basename, i);
3231       if (!newDest.exists() && (!download.is(RUNNING) || !Dialog.checkSameName(this, newDest.path))) {
3232         break;
3233       }
3234     }
3235     cur.newDest = newDest.leafName;
3236     cur.conflicts = i; 
3237   },
3238   _returnFromDialog: function CM__returnFromDialog(option, type) {
3239     if (type == 1) {
3240       this._sessionSetting = option;
3241     }
3242     if (type == 2) {
3243       Preferences.setExt('conflictresolution', option);
3244     }   
3245     this._return(option);
3246   },
3247   _return: function CM__return(option) {
3248     let cur = this._items[0];
3249     switch (option) {
3250       /* rename */    case 0: this._computeConflicts(cur); cur.download.conflicts = cur.conflicts; break;
3251       /* overwrite */ case 1: cur.download.shouldOverwrite = true; break;
3252       /* skip */      default: cur.download.cancel(_('skipped')); break;
3253     }
3254     if (cur.reentry) {
3255       cur.download[cur.reentry]();
3256     }
3257     this._items.shift();
3258     this._processing = false;
3259     this._process();
3260   }
3261 };
3262
3263 addEventListener(
3264   "load",
3265   function() {
3266     if (!Preferences.getExt('startminimized', false)) {
3267       return;
3268     }
3269     // Only start minimized if invoked with new downloads
3270     if (!window.arguments || !window.arguments.length) {
3271       return;
3272     }
3273     setTimeout(
3274       function() {
3275         try {
3276           window.QueryInterface(Ci.nsIDOMChromeWindow).minimize();
3277           if (window.opener) {
3278             window.opener.focus();
3279           }
3280         }
3281         catch (ex) {
3282         }
3283       },
3284       0
3285     );
3286   },
3287   false
3288 );
Note: See TracBrowser for help on using the browser.