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

Revision 815, 63.0 kB (checked in by MaierMan, 3 years ago)

*flat* renaming masks

#340: renaming scheme *flatsubdirs*
#143: Renaming Mask *curl* alternative option

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 <f.parodi@tiscali.it>
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_ERROR_MODULE_NETWORK = 0x804B0000;
40 const NS_ERROR_BINDING_ABORTED = NS_ERROR_MODULE_NETWORK + 2;
41 const NS_ERROR_UNKNOWN_HOST = NS_ERROR_MODULE_NETWORK + 30;
42 const NS_ERROR_CONNECTION_REFUSED = NS_ERROR_MODULE_NETWORK + 13;
43 const NS_ERROR_NET_TIMEOUT = NS_ERROR_MODULE_NETWORK + 14;
44 const NS_ERROR_NET_RESET = NS_ERROR_MODULE_NETWORK + 20;
45
46 const Cc = Components.classes;
47 const Ci = Components.interfaces;
48
49 const Exception = Components.Exception;
50 const Construct = Components.Constructor;
51 function Serv(c, i) {
52   return Cc[c].getService(i ? Ci[i] : null);
53 }
54
55 const BufferedOutputStream = Construct('@mozilla.org/network/buffered-output-stream;1', 'nsIBufferedOutputStream', 'init');
56 const BinaryOutputStream = Construct('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream');
57 const BinaryInputStream = Construct('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream');
58 const FileInputStream = Construct('@mozilla.org/network/file-input-stream;1', 'nsIFileInputStream', 'init');
59
60 const MimeService = Serv('@mozilla.org/uriloader/external-helper-app-service;1', 'nsIMIMEService');
61 const ObserverService = Serv('@mozilla.org/observer-service;1', 'nsIObserverService');
62 const WindowWatcherService = Serv('@mozilla.org/embedcomp/window-watcher;1', 'nsIWindowWatcher');
63
64 const MIN_CHUNK_SIZE = 512 * 1024;
65
66 // ammount to buffer in BufferedOutputStream
67 // furthermore up to this ammount will automagically discared after crashes
68 const CHUNK_BUFFER_SIZE = 96 * 1024;
69
70 // in use by chunk.writer...
71 // in use by decompressor... beware, actual size might be more than twice as big!
72 const MAX_BUFFER_SIZE = 5 * 1024 * 1024;
73 const MIN_BUFFER_SIZE = 1 * 1024 * 1024;
74
75 const REFRESH_FREQ = 1000;
76 const REFRESH_NFREQ = 1000 / REFRESH_FREQ;
77 const STREAMS_FREQ = 100;
78
79 var Dialog = {
80   _observes: ['quit-application-requested', 'quit-application-granted'],
81   _initialized: false,
82   _wasRunning: false,
83   _lastTime: Utils.getTimestamp(),
84   _running: [],
85   _autoClears: [],
86   completed: 0,
87   totalbytes: 0,
88   init: function D_init() {
89     Tree.init($("downloads"));
90     makeObserver(this);
91    
92     this._observes.forEach(
93       function(topic) {
94         ObserverService.addObserver(this, topic, true);
95       },
96       this
97     );
98  
99     document.getElementById("dtaHelp").hidden = !("openHelp" in window);
100  
101     SessionManager.init();
102  
103     if ("arguments" in window) {
104       startDownloads(window.arguments[0], window.arguments[1]);
105     }
106
107     Tree.invalidate();
108     this._initialized = true;
109     for (let d in Tree.all) {
110       if (d.is(FINISHING)) {
111         this.run(d);
112       }
113     }
114     this._updTimer = new Timer("Dialog.checkDownloads();", REFRESH_FREQ, true, true);
115     new Timer("Dialog.refreshWritten();", 100, true, true);
116     new Timer("Dialog.saveRunning();", 10000, true);
117   },
118   observe: function D_observe(subject, topic, data) {
119     if (topic == 'quit-application-requested') {
120       if (!this._canClose()) {
121         delete this._forceClose;
122         try {
123           let cancelQuit = subject.QueryInterface(Ci.nsISupportsPRBool);
124           cancelQuit.data = true;
125         }
126         catch (ex) {
127           Debug.log("cannot set cancelQuit", ex);
128         }
129       }
130     }
131     else if (topic == 'quit-application-granted') {
132       this._forceClose = true;
133     }
134   },
135   refresh: function D_refresh() {
136     try {
137       let sum = 0;
138       const now = Utils.getTimestamp();
139       this._running.forEach(
140         function(i) {
141           let d = i.d;
142          
143           let advanced = (d.partialSize - i.lastBytes);
144           sum += advanced;
145          
146           let elapsed = (now - i.lastTime) / 1000;         
147           if (elapsed < 1) {
148             return;
149           }           
150          
151           let speed = Math.round(advanced / elapsed);
152          
153           i.lastBytes = d.partialSize;
154           i.lastTime = now;       
155
156           // Refresh item speed
157           d.speeds.push(speed > 0 ? speed : 0);
158           if (d.speeds.length > SPEED_COUNT) {
159             d.speeds.shift();
160           }
161           i.lastBytes = d.partialSize;
162           i.lastTime = now;
163          
164           speed = 0;
165           d.speeds.forEach(
166             function(s) {
167               speed += s;
168             }
169           );
170           speed /= d.speeds.length;
171          
172           // Calculate estimated time         
173           if (advanced != 0 && d.totalSize > 0) {
174             let remaining = Math.ceil((d.totalSize - d.partialSize) / speed);
175             if (!isFinite(remaining)) {
176               d.status = _("unknown");
177             }
178             else {
179               d.status = Utils.formatTimeDelta(remaining);
180             }
181           }
182           d.speed = Utils.formatBytes(speed) + "/s";
183         }
184       );
185       let elapsed = (now - this._lastTime) / 1000;
186       this._lastTime = now;
187       let speed = Math.round(sum * elapsed);
188       speed = Utils.formatBytes((speed > 0) ? speed : 0);
189
190       // Refresh status bar
191       $("statusText").label =
192         _("cdownloads", [this.completed, Tree.rowCount])
193         + " - "
194         + _("cspeed")
195         + " "
196         + speed + "/s";
197
198       // Refresh window title
199       if (this._running.length == 1 && this._running[0].d.totalSize > 0) {
200         document.title =
201           this._running[0].d.percent
202           + ' - '
203           + this.completed + "/" + Tree.rowCount + " - "
204           + speed + '/s - DownThemAll!';
205       }
206       else if (this._running.length > 0) {
207         document.title =
208           Math.floor(this.completed * 100 / Tree.rowCount) + '%'
209           + ' - '       
210           + this.completed + "/" + Tree.rowCount + " - "
211           + speed + '/s - DownThemAll!';
212       }
213       else {
214         document.title = this.completed + "/" + Tree.rowCount + " - DownThemAll!";
215       }
216     }
217     catch(ex) {
218       Debug.log("refresh():", ex);
219     }
220   },
221   refreshWritten: function D_checkDownloads() {
222     this._running.forEach(
223       function(i) {
224         i.d.invalidate();
225       }
226     );
227   },
228   saveRunning: function D_saveRunning() {
229     if (!this._running.length) {
230       return;
231     }
232     SessionManager.beginUpdate();
233     this._running.forEach(
234       function(i) {
235           SessionManager.save(i.d);
236       }
237     );
238     SessionManager.endUpdate();
239   },
240
241   checkDownloads: function D_checkDownloads() {
242     try {
243       this.refresh();
244      
245       if (Prefs.autoClearComplete && this._autoClears.length) {
246         Tree.remove(this._autoClears);
247         this._autoClears = [];
248       }
249      
250       if (Prefs.autoRetryInterval) {
251         for (let d in Tree.all) {
252           d.autoRetry();
253         }
254       }
255          
256       this._running.forEach(
257         function(i) {
258           let d = i.d;
259           // checks for timeout
260           if (d.is(RUNNING) && (Utils.getTimestamp() - d.timeLastProgress) >= Prefs.timeout * 1000) {
261             if (d.resumable || !d.totalSize || !d.partialSize) {
262               d.pause();
263               d.markAutoRetry();
264               d.status = _("timeout");
265             }
266             else {
267               d.cancel(_("timeout"));
268             }
269             Debug.logString(d + " is a timeout");
270           }
271         }
272       )
273       this.startNext();
274     }
275     catch(ex) {
276       Debug.log("checkDownloads():", ex);
277     }
278   },
279   checkSameName: function D_checkSameName(download, path) {
280     for (let i = 0; i < this._running.length; ++i) {
281       if (this._running[i].d == download) {
282         continue;
283       }
284       if (this._running[i].d.destinationFile == path) {
285         return true;
286       }
287     }
288     return false;
289   },
290   startNext: function D_startNext() {
291     try {
292       var rv = false;
293       for (let d in Tree.all) {
294         if (this._running.length >= Prefs.maxInProgress) {
295           return rv;
296         }       
297         if (!d.is(QUEUED)) {
298           continue;
299         }
300         this.run(d);
301         rv = true;
302       }
303       return rv;
304     }
305     catch(ex){
306       Debug.log("startNext():", ex);
307     }
308     return false;
309   },
310   RunningJob: function(d) {
311     this.d = d;
312     this.lastBytes = d.partialSize;
313     this.lastTime = Utils.getTimestamp();
314   },
315   run: function D_run(download) {
316     download.status = _("starting");
317     if (download.is(FINISHING) || (download.partialSize >= download.totalSize && download.totalSize)) {
318       // we might encounter renaming issues;
319       // but we cannot handle it because we don't know at which stage we crashed
320       download.partialSize = download.totalSize;
321       Debug.logString("Download seems to be complete; likely a left-over from a crash, finish it:" + download);
322       download.finishDownload();
323       return;
324     }
325     download.timeLastProgress = Utils.getTimestamp();
326     download.timeStart = Utils.getTimestamp();
327     download.state = RUNNING;
328     if (!download.started) {
329       download.started = true;
330       Debug.logString("Let's start " + download);
331     }
332     else {
333       Debug.logString("Let's resume " + download + " at " + download.partialSize);
334     }
335     this._running.push(new Dialog.RunningJob(download));
336     download.resumeDownload();
337   },
338   wasStopped: function D_wasStopped(download) {
339     this._running = this._running.filter(
340       function(i) {
341         if (i.d == download) {
342           return false;
343         }
344         return true;
345       },
346       this
347     );
348   },
349   signal: function D_signal(download) {
350     SessionManager.save(download);
351     if (download.is(RUNNING)) {
352       this._wasRunning = true;
353     }
354     else if (Prefs.autoClearComplete && download.is(COMPLETE)) {
355       this._autoClears.push(download);
356     }
357     if (!this._initialized || !this._wasRunning || !download.is(COMPLETE)) {
358       return;
359     }
360     try {
361       // check if there is something running or scheduled
362       if (this.startNext() || Tree.some(function(d) { return d.is(FINISHING, RUNNING, QUEUED); } )) {
363         return;
364       }
365       Debug.logString("signal(): Queue finished");
366       Utils.playSound("done");
367      
368       let dp = Tree.at(0);
369       if (dp) {
370         dp = dp.destinationPath;
371       }
372       if (Prefs.alertingSystem == 1) {
373         AlertService.show(_("dcom"), _('suc'), dp, dp);
374       }
375       else if (dp && Prefs.alertingSystem == 0) {
376         if (confirm(_('suc') + "\n "+ _("folder")) == 1) {
377           try {
378             OpenExternal.launch(dp);
379           }
380           catch (ex){
381             // no-op
382           }
383         }
384       }
385       if (Prefs.autoClose) {
386         Dialog.close();
387       }
388     }
389     catch(ex) {
390       Debug.log("signal():", ex);
391     }
392   },
393   _canClose: function D__canClose() {
394     if (Tree.some(function(d) { return d.started && !d.resumable && d.is(RUNNING); })) {
395       var rv = DTA_confirmYN(
396         _("confclose"),
397         _("nonres")
398       );
399       if (rv) {
400         return false;
401       }
402     }
403     return (this._forceClose = true);
404   },
405   close: function D_close() {
406     Debug.logString("Close request");
407     if (!this._forceClose && !this._canClose()) {
408       delete this._forceClose;
409       return false;
410     }
411
412     // stop everything!
413     // enumerate everything we'll have to wait for!
414     if (this._updTimer) {
415       this._updTimer.kill();
416       delete this._updTimer;
417     }
418     let chunks = 0;
419     let finishing = 0;
420     Tree.updateAll(
421       function(d) {
422         if (d.is(RUNNING, QUEUED)) {
423           // enumerate all running chunks
424           d.chunks.forEach(
425             function(c) {
426               if (c.running) {
427                 ++chunks;
428               }
429             },
430             this
431           );
432           d.pause();       
433         }
434         else if (d.is(FINISHING)) {
435           ++finishing;
436         }
437       },
438       this
439     );
440     if (chunks || finishing) {
441       if (this._safeCloseAttempts < 20) {
442         ++this._safeCloseAttempts;
443         new Timer(function() { Dialog.close(); }, 250);       
444         return false;
445       }
446       Debug.logString("Going down even if queue was not probably closed yet!");
447     }
448     close();
449     return true;
450   },
451   _cleanTmpDir: function D__cleanTmpDir() {
452     if (!Prefs.tempLocation || Preferences.getMultiByteDTA("tempLocation", '') != '') {
453       // cannot perform this action if we don't use a temp file
454       // there might be far too many directories containing far too many tmpFiles.
455       // or part files from other users.
456       return;
457     }
458     let known = [];
459     for (d in Tree.all) {
460       known.push(d.tmpFile.leafName);
461     }
462     let tmpEnum = Prefs.tempLocation.directoryEntries;
463     let unknown = []
464     while (tmpEnum.hasMoreElements()) {
465       let f = tmpEnum.getNext().QueryInterface(Ci.nsILocalFile);
466       if (f.leafName.match(/\.dtapart$/) && known.indexOf(f.leafName) == -1) {
467         unknown.push(f);
468       }
469     }
470     unknown.forEach(
471       function(f) {
472         try {
473           f.remove(false);
474         }
475         catch(ex) {
476         }
477       }
478     );
479   },
480   _safeCloseAttempts: 0,
481
482   unload: function D_unload() {
483     TimerManager.killAll();
484     try {
485       this._cleanTmpDir();
486     }
487     catch(ex) {
488       Debug.log("_safeClose", ex);
489     }
490     SessionManager.shutdown();
491     return true;   
492   }
493 };
494
495 function UrlManager(urls) {
496   this._urls = [];
497   this._idx = -1;
498
499   if (urls instanceof Array) {
500     this.initByArray(urls);
501     this._hasFresh = this._urls.length != 0;
502   }
503   else if (urls) {
504     throw "Feeding the UrlManager with some bad stuff is usually a bad idea!";
505   }
506 }
507 UrlManager.prototype = {
508   _sort: function(a,b) {
509     const rv = b.preference - a.preference;
510     return rv ? rv : (a.url < b.url ? -1 : 1);
511   },
512   initByArray: function um_initByArray(urls) {
513     for (let i = 0; i < urls.length; ++i) {
514       this.add(
515         new DTA_URL(
516           urls[i].url,
517           urls[i].charset,
518           urls[i].usable,
519           urls[i].preference
520         )
521       );
522     }
523     this._urls.sort(this._sort);
524     this._usable = this._urls[0].usable;
525   },
526   add: function um_add(url) {
527     if (!url instanceof DTA_URL) {
528       throw (url + " is not an DTA_URL");
529     }
530     if (!this._urls.some(function(ref) { return ref.url == url.url; })) {
531       this._urls.push(url);
532     }
533   },
534   getURL: function um_getURL(idx) {
535     if (typeof(idx) != 'number') {
536       this._idx++;
537       if (this._idx >= this._urls.length) {
538         this._idx = 0;
539       }
540       idx = this._idx;
541     }
542     return this._urls[idx];
543   },
544   get url() {
545     return this._urls[0].url;
546   },
547   get usable() {
548     return this._urls[0].usable;
549   },
550   get charset() {
551     return this._urls[0].charset;
552   },
553   get length() {
554     return this._urls.length;
555   },
556   markBad: function um_markBad(url) {
557     if (this._urls.length > 1) {
558       this._urls = this._urls.filter(function(u) { return u != url; });
559     }
560     else if (this._urls[0] == url) {
561       return false;
562     }
563     return true;
564   },
565   toSource: function um_toSource() {
566     let rv = [];
567     this._urls.forEach(
568       function(url) {
569         rv.push({
570           'url': url.url,
571           'charset': url.charset,
572           'usable': url.usable,
573           'preference': url.preference
574         });
575       }
576     );
577     return rv;
578   },
579   toString: function() {
580     let rv = '';
581     this._urls.forEach(
582       function(u) {
583         rv += u.preference + " " + u.url + "\n";
584       }
585     );
586     return rv;
587   }
588 };
589 function Visitor() {
590   // sanity check
591   if (arguments.length != 1) {
592     return;
593   }
594
595   var nodes = arguments[0];
596   for (x in nodes) {
597     if (!name || !(name in this.cmpKeys)) {
598       continue;
599     }
600     this[x] = nodes[x];
601   }
602 }
603
604 Visitor.prototype = {
605   cmpKeys: {
606     'etag': true, // must not be modified from 200 to 206: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.7
607     //'content-length': false,
608     'content-type': true,
609     'last-modified': true, // may get omitted later, but should not change
610     'content-encoding': true // must not change, or download will become corrupt.
611   },
612   type: null,
613   overrideCharset: null,
614   encoding: null,
615   fileName: null,
616   acceptRanges: 'bytes',
617   contentlength: 0,
618   time: null,
619
620   QueryInterface: function(aIID) {
621     if (
622       aIID.equals(Ci.nsISupports)
623       || aIID.equals(Ci.nsIHttpHeaderVisitor)
624     ) {
625       return this;
626     }
627     throw Components.results.NS_ERROR_NO_INTERFACE;
628   },
629   visitHeader: function(aHeader, aValue) {
630     try {
631       const header = aHeader.toLowerCase();
632       switch (header) {
633         case 'content-type': {
634           this.type = aValue;
635           var ch = aValue.match(/charset=['"]?([\w\d_-]+)/i);
636           if (ch && ch[1].length) {
637             DTA_debug.logString("visitHeader: found override to " + ch[1]);
638             this.overrideCharset = ch[1];
639           }
640         }
641         break;
642
643         case 'content-encoding':
644           this.encoding = aValue;
645         break;
646
647         case 'accept-ranges':
648           this.acceptRanges = aValue.toLowerCase().indexOf('none') == -1;
649           Debug.logString("acceptrange = " + aValue.toLowerCase());
650         break;
651
652         case 'content-length':
653           this.contentlength = Number(aValue);
654         break;
655
656         case 'content-range': {
657           let cl = new Number(aValue.split('/').pop());
658           if (cl > 0) {
659             this.contentlength = cl;
660           }
661         }
662         break;
663         case 'last-modified':
664           try {
665             this.time = Utils.getTimestamp(aValue);
666           }
667           catch (ex) {
668             Debug.log("gts", ex);
669           }
670         break;
671       }
672       if (header == 'etag') {
673         // strip off the "inode"-part apache and others produce, as mirrors/caches usually provide different/wrong numbers here :p
674         this[header] = aValue
675           .replace(/^(?:[Ww]\/)?"(.+)"$/, '$1')
676           .replace(/^[a-f\d]+-([a-f\d]+)-([a-f\d]+)$/, '$1-$2')
677           .replace(/^([a-f\d]+):[a-f\d]{1,6}$/, '$1');
678           Debug.logString("Etag: " + this[header] + " - " + aValue);
679       }
680       else if (header in this.cmpKeys) {
681         this[header] = aValue;
682       }
683       if ((header == 'content-type' || header == 'content-disposition') && this.fileName == null) {
684         // we have to handle headers like "content-disposition: inline; filename='dummy.txt'; title='dummy.txt';"
685         var value = aValue.match(/file(?:name)?=(["']?)([^\1;]+)\1(?:;.+)?/i);
686         if (!value) {
687           // workaround for bug #13959
688           // attachments on some vbulletin forums send nasty headers like "content-disposition: inline; filename*=utf-8''file.ext"
689           value = aValue.match(/file(?:name)?\*=(.*)''(.+)/i);
690           if (value) {
691             this.overrideCharset = value[1];
692           }
693         }
694         if (value) {
695           this.fileName = value[2].getUsableFileName();
696         }
697       }
698     }
699     catch (ex) {
700       Debug.log("hrhv::visitHeader:", ex);
701     }
702   },
703   compare: function vi_compare(v) {
704     if (!(v instanceof Visitor)) {
705       return;
706     }
707
708     for (x in this.cmpKeys) {
709       // we don't have this header
710       if (!(x in this)) {
711         continue;
712       }
713       // v does not have this header
714       else if (!(x in v)) {
715         // allowed to be missing?
716         if (this.cmpKeys[x]) {
717           continue;
718         }
719         Debug.logString(x + " missing");
720         throw new Exception(x + " is missing");
721       }
722       // header is there, but differs
723       else if (this[x] != v[x]) {
724         Debug.logString(x + " nm: [" + this[x] + "] [" + v[x] + "]");
725         throw new Exception("Header " + x + " doesn't match");
726       }
727     }
728   },
729   save: function vi_save(node) {
730     var rv = {};
731     // salva su file le informazioni sugli headers
732     for (x in this.cmpKeys) {
733       if (!(x in this)) {
734         continue;
735       }
736       rv[x] = this[x];
737     }
738     return rv;
739   }
740 };
741
742 /**
743  * Visitor Manager c'tor
744  * @author Nils
745  */
746 function VisitorManager(nodes) {
747   this._visitors = {};
748   if (nodes) {
749     this._load(nodes);
750   }
751 }
752 VisitorManager.prototype = {
753   /**
754    * Loads a ::save'd JS Array
755    * Will silently bypass failed items!
756    * @author Nils
757    */
758   _load: function vm_init(nodes) {
759     for (let i = 0; i < nodes.length; ++i) {
760       try {
761         this._visitors[nodes[i].url] = new Visitor(nodes[i].values);
762       }
763       catch (ex) {
764         Debug.log("failed to read one visitor", ex);
765       }
766     }
767   },
768   /**
769    * Saves/serializes the Manager and associated Visitors to an JS Array
770    * @return A ::load compatible Array
771    * @author Nils
772    */
773   toSource: function vm_toSource() {
774     var rv = [];
775     for (let x in this._visitors) {
776       try {
777         var v = {};
778         v.url = x;
779         v.values = this._visitors[x].save();
780         rv.push(v);
781       }
782       catch(ex) {
783         Debug.log(x, ex);
784       }
785     }
786     return rv;
787   },
788   /**
789    * Visit and compare a channel
790    * @returns visitor for channel
791    * @throws Exception if comparision yield a difference (i.e. channels are not "compatible")
792    * @author Nils
793    */
794   visit: function vm_visit(chan) {
795     var url = chan.URI.spec;
796
797     var visitor = new Visitor();
798     chan.visitResponseHeaders(visitor);
799     if (url in this._visitors)
800     {
801         this._visitors[url].compare(visitor);
802     }
803     return (this._visitors[url] = visitor);
804   },
805   /**
806    * return the first timestamp registered with a visitor
807    * @throws Exception if no timestamp found
808    * @author Nils
809    */
810   get time() {
811     for (let i in this._visitors) {
812       if (this._visitors[i].time > 0) {
813         return this._visitors[i].time;
814       }
815     }
816     throw new Exception("No Date registered");
817   }
818 };
819
820 function QueueItem(lnk, dir, num, desc, mask, referrer, tmpFile) {
821
822   this.visitors = new VisitorManager();
823
824   dir = dir.addFinalSlash();
825
826   if (typeof lnk == 'string') {
827     this.urlManager = new UrlManager([new DTA_URL(lnk)]);
828   }
829   else if (lnk instanceof UrlManager) {
830     this.urlManager = lnk;
831   }
832   else {
833     this.urlManager = new UrlManager([lnk]);
834   }
835
836   this.startDate = new Date(); 
837   this.numIstance = num;
838   this.description = desc ? desc : '';
839   this.chunks = [];
840   this.speeds = new Array();
841  
842   if (referrer) {
843     try {
844       this.referrer = referrer.toURL();
845     }
846     catch (ex) {
847       // We might have been fed with about:blank or other crap. so ignore.
848     }
849   }
850
851   // only access the setter of the last so that we don't generate stuff trice.
852   this._pathName = dir;
853   this._mask = mask;
854   this.fileName = this.urlManager.usable.getUsableFileName();
855  
856   if (tmpFile) {
857     try {
858       tmpFile = new FileFactory(tmpFile);
859       if (tmpFile.exists()) {
860         this._tmpFile = tmpFile;
861       }
862       else {
863         // Download partfile is gone!
864         // XXX find appropriate error message!
865         this.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
866       }
867     }
868     catch (ex) {
869       Debug.log("tried to construct with invalid tmpFile", ex);
870       this.cancel();
871     }
872   }
873 }
874
875 QueueItem.prototype = {
876   _state: QUEUED,
877   get state() {
878     return this._state;
879   },
880   set state(nv) {
881     if (this._state != nv) {
882       if (this._state == RUNNING) {
883         // remove ourself from inprogresslist
884         Dialog.wasStopped(this);
885       }
886       this._state = nv;
887       this.invalidate();
888       Tree.refreshTools();
889       Dialog.signal(this);
890     }
891   },
892  
893   _fileName: null,
894   get fileName() {
895     return this._fileName;
896   },
897   set fileName(nv) {
898     this._fileName = nv;
899     this.rebuildDestination();
900     this.invalidate();
901     return nv;
902   },
903
904   _pathName: null,
905   get pathName() {
906     return this._pathName;
907   },
908   set pathName(nv) {
909     this._pathName = nv;
910     this.rebuildDestination();
911     this.invalidate();
912     return nv;
913   }, 
914
915   _mask: null,
916   get mask() {
917     return this._mask;
918   },
919   set mask(nv) {
920     this._mask = nv;
921     this.rebuildDestination();
922     this.invalidate();
923     return nv;
924   },   
925  
926   _destinationName: null,
927   destinationNameOverride: null,
928   _destinationNameFull: null,
929   get destinationName() {
930     return this._destinationNameFull;
931   },
932   set destinationName(nv) {
933     this.destinationNameOverride = nv;
934     this.rebuildDestination();
935     this.invalidate();
936     return this._destinationNameFull;
937   },
938  
939   _destinationFile: null,
940   get destinationFile() {
941     if (!this._destinationFile) {
942       this.rebuildDestination();
943     }
944     return this._destinationFile;
945   },
946  
947   _conflicts: 0,
948   get conflicts() {
949     return this._conflicts;
950   },
951   set conflicts(nv) {
952     if (typeof(nv) != 'number') {
953       return this._conflicts;
954     }
955     this._conflicts = nv;
956     this.rebuildDestination();
957     this.invalidate();
958     return nv;
959   },
960   _tmpFile: null,
961   get tmpFile() {
962     if (!this._tmpFile) {
963       var dest = Prefs.tempLocation
964         ? Prefs.tempLocation.clone()
965         : new FileFactory(this.destinationPath);
966       let name = this.fileName;
967       if (name.length > 60) {
968         name = name.substring(0, 60);
969       }
970       dest.append(name + "-" + newUUIDString() + '.dtapart');
971       this._tmpFile = dest;
972     }
973     return this._tmpFile;
974   },
975   _hash: null,
976   get hash() {
977     return this._hash;
978   },
979   set hash(nv) {
980     this._hash = nv;
981     this._prettyHash = this.hash ? _('prettyhash', [this.hash.type, this.hash.sum]) : _('nas');
982   },
983   _prettyHash: null,
984   get prettyHash() {
985     return this._prettyHash;
986   },
987
988   /**
989    *Takes one or more state indicators and returns if this download is in state of any of them
990    */
991   is: function QI_is() {
992     let state = this.state;
993     for (let i = 0, e = arguments.length; i < e; ++i) {
994       if (state == arguments[i]) {
995         return true;
996       }
997     }
998     return false;
999   },
1000
1001   contentType: "",
1002   visitors: null,
1003   _totalSize: 0,
1004   get totalSize() { return this._totalSize; },
1005   set totalSize(nv) {
1006     this._totalSize = nv;
1007     this.invalidate();
1008     return this._totalSize;
1009   },
1010   partialSize: 0,
1011
1012   startDate: null,
1013
1014   compression: null,
1015
1016   resumable: true,
1017   started: false,
1018
1019   _activeChunks: 0,
1020   get activeChunks() {
1021     return this._activeChunks;
1022   },
1023   set activeChunks(nv) {
1024     nv = Math.max(0, nv);
1025     this._activeChunks = nv;
1026     this.invalidate();
1027     return this._activeChunks;
1028   },
1029   _maxChunks: 0,
1030   get maxChunks() {
1031     if (!this._maxChunks) {
1032         this._maxChunks = Prefs.maxChunks;
1033     }
1034     return this._maxChunks;
1035   },
1036   set maxChunks(nv) {
1037     this._maxChunks = nv;
1038     if (this._maxChunks < this._activeChunks) {
1039       let running = this.chunks.filter(function(c) { return c.running; });
1040       while (running.length && this._maxChunks < running.length) {
1041         let c = running.pop();
1042         if (c.remainder < 10240) {
1043           continue;
1044         }
1045         c.cancel();
1046       }
1047     }
1048     else if (this._maxChunks > this._activeChunks && this.is(RUNNING)) {
1049       this.resumeDownload();
1050      
1051     }
1052     this.invalidate();
1053     Debug.logString("mc set to " + nv);
1054     return this._maxChunks;
1055   },
1056   timeLastProgress: 0,
1057   timeStart: 0,
1058
1059   _icon: null,
1060   get icon() {
1061     if (!this._icon) {
1062       this._icon = getIcon(this.destinationName, 'metalink' in this);
1063     }
1064     return this._icon;
1065   },
1066   get largeIcon() {
1067     return getIcon(this.destinationName, 'metalink' in this, 32);
1068   },
1069   get size() {
1070     try {
1071       let file = new FileFactory(this.destinationFile);
1072       if (file.exists()) {
1073         return file.fileSize;
1074       }
1075     }
1076     catch (ex) {
1077       Debug.log("download::getSize(): ", e)
1078     }
1079     return 0;
1080   },
1081   get dimensionString() {
1082     if (this.partialSize <= 0) {
1083       return _('unknown');
1084     }
1085     else if (this.totalSize <= 0) {
1086       return _('transfered', [Utils.formatBytes(this.partialSize), _('nas')]);
1087     }
1088     else if (this.is(COMPLETE)) {
1089       return Utils.formatBytes(this.totalSize);
1090     }
1091     return _('transfered', [Utils.formatBytes(this.partialSize), Utils.formatBytes(this.totalSize)]);
1092   },
1093   _status : '',
1094   get status() {
1095     return this._status + (this._autoRetryTime ? ' *' : '');
1096   },
1097   set status(nv) {
1098     if (nv != this._status) {
1099       this._status = nv;
1100       this.invalidate();
1101     }
1102     return this._status;
1103   },
1104   get parts() {
1105     if (this.maxChunks) {
1106       return (this.activeChunks) + '/' + this.maxChunks;
1107     }
1108     return '';
1109   },
1110   get percent() {
1111     if (!this.totalSize && this.is(RUNNING)) {
1112       return _('nas');
1113     }
1114     else if (!this.totalSize) {
1115       return "0%";
1116     }
1117     else if (this.is(COMPLETE)) {
1118       return "100%";
1119     }
1120     return Math.floor(this.partialSize / this.totalSize * 100) + "%";
1121   },
1122   _destinationPath: '',
1123   get destinationPath() {
1124     return this._destinationPath;
1125   },
1126
1127   invalidate: function QI_invalidate() {
1128     Tree.invalidate(this);
1129   },
1130
1131   safeRetry: function QI_safeRetry() {
1132     // reset flags
1133     this.totalSize = this.partialSize = 0;
1134     this.compression = null;
1135     this.activeChunks = this.maxChunks = 0;
1136     this.chunks.forEach(function(c) { c.cancel(); });
1137     this.chunks = [];
1138     this.speeds = [];
1139     this.visitors = new VisitorManager();
1140     Dialog.run(this);
1141   },
1142
1143   refreshPartialSize: function QI_refreshPartialSize(){
1144     var size = 0;
1145     this.chunks.forEach(function(c) { size += c.written; });
1146     this.partialSize = size;
1147   },
1148
1149   pause: function QI_pause(){
1150     if (this.chunks) {
1151       for (let i = 0, e = this.chunks.length; i < e; ++i) {
1152         if (this.chunks[i].running) {
1153           this.chunks[i].cancel();
1154         }
1155       }
1156     }
1157     this.activeChunks = 0;
1158     this.state = PAUSED;
1159     this.speeds = [];
1160   },
1161
1162   moveCompleted: function QI_moveCompleted() {
1163     if (this.is(CANCELED)) {
1164       return;
1165     }
1166     ConflictManager.resolve(this, 'continueMoveCompleted');
1167   },
1168   continueMoveCompleted: function QI_continueMoveCompleted() {
1169     if (this.is(CANCELED)) {
1170       return;
1171     }   
1172     try {
1173       // safeguard against some failed chunks.
1174       this.chunks.forEach(function(c) { c.close(); });
1175       var destination = new FileFactory(this.destinationPath);
1176       Debug.logString(this.fileName + ": Move " + this.tmpFile.path + " to " + this.destinationFile);
1177
1178       if (!destination.exists()) {
1179         destination.create(Ci.nsIFile.DIRECTORY_TYPE, 0766);
1180         this.invalidate();
1181       }
1182       var df = destination.clone();
1183       df.append(this.destinationName);
1184       if (df.exists()) {
1185         df.remove(false);
1186       }
1187       // move file
1188       if (this.compression) {
1189         DTA_include("dta/manager/decompressor.js");
1190         new Decompressor(this);
1191       }
1192       else {
1193         this.tmpFile.clone().moveTo(destination, this.destinationName);
1194         this.complete();
1195       }
1196     }
1197     catch(ex) {
1198       Debug.log("continueMoveCompleted encountered an error", ex);
1199       this.complete(ex);
1200     }
1201   },
1202   handleMetalink: function QI_handleMetaLink() {
1203     try {
1204       DTA_include("dta/manager/metalinker.js");
1205       Metalinker.handleDownload(this);
1206     }
1207     catch (ex) {
1208       Debug.log("handleMetalink", ex);
1209     }
1210   },
1211   verifyHash: function() {
1212     DTA_include("dta/manager/verificator.js");
1213     new Verificator(this);
1214   },
1215   customFinishEvent: function() {
1216     DTA_include("dta/manager/customevent.js");
1217     new CustomEvent(this, Prefs.finishEvent);
1218   },
1219   setAttributes: function() {
1220     if (Prefs.setTime) {
1221       try {
1222         var time = this.startDate.getTime();
1223         try {
1224           var time =  this.visitors.time;
1225         }
1226         catch (ex) {
1227           // no-op
1228         }
1229         // small validation. Around epoche? More than a month in future?
1230         if (time < 2 || time > Date.now() + 30 * 86400000) {
1231           throw new Exception("invalid date encountered: " + time + ", will not set it");
1232         }
1233         // have to unwrap
1234         var file = new FileFactory(this.destinationFile);
1235         file.lastModifiedTime = time;
1236       }
1237       catch (ex) {
1238         Debug.log("Setting timestamp on file failed: ", ex);
1239       }
1240     }
1241     this.totalSize = this.partialSize = this.size;
1242     ++Dialog.completed;
1243    
1244     this.complete();
1245   },
1246   finishDownload: function QI_finishDownload(exception) {
1247     Debug.logString("finishDownload, connections: " + this.sessionConnections);
1248     this._completeEvents = ['moveCompleted', 'setAttributes'];
1249     if (this.hash) {
1250       this._completeEvents.push('verifyHash');
1251     }
1252     if ('isMetalink' in this) {
1253       this._completeEvents.push('handleMetalink');
1254     }
1255     if (Prefs.finishEvent) {
1256       this._completeEvents.push('customFinishEvent');
1257     }
1258     this.complete();
1259   },
1260   _completeEvents: [],
1261   complete: function QI_complete(exception) {
1262     if (exception) {
1263       this.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
1264       Debug.log("complete: ", exception);
1265       return;
1266     }
1267     if (this._completeEvents.length) {
1268       var evt = this._completeEvents.shift();
1269       var tp = this;
1270       window.setTimeout(
1271         function() {
1272           try {
1273             tp[evt]();
1274           }
1275           catch(ex) {
1276             Debug.log("completeEvent failed: " + evt, ex);
1277             tp.complete();
1278           }
1279         },
1280         0
1281       );
1282       return;
1283     }
1284     this.chunks = [];   
1285     this.activeChunks = 0;
1286     this.state = COMPLETE;
1287     this.status = _("complete");
1288   },
1289   rebuildDestination: function QI_rebuildDestination() {
1290     try {
1291       let uri = this.urlManager.usable.toURL();
1292       let host = uri.host.toString();
1293
1294       // normalize slashes
1295       let mask = this.mask
1296         .normalizeSlashes()
1297         .removeLeadingSlash()
1298         .removeFinalSlash();
1299
1300       let uripath = uri.path.removeLeadingChar("/");
1301       if (uripath.length) {
1302         uripath = uripath.substring(0, uri.path.lastIndexOf("/"))
1303           .normalizeSlashes()
1304           .removeFinalSlash();
1305       }
1306
1307       let query = '';
1308       try {
1309         query = uri.query;
1310       }
1311       catch (ex) {
1312         // no-op
1313       }
1314
1315       let description = this.description.removeBadChars().replaceSlashes(' ').trim();
1316      
1317       let name = this.fileName;
1318       let ext = name.getExtension();
1319       if (ext) {
1320         name = name.substring(0, name.length - ext.length - 1);
1321
1322         if (this.contentType && /htm/.test(this.contentType) && !/htm/.test(ext)) {
1323           ext += ".html";
1324         }
1325       }
1326       // mime-service method
1327       else if (this.contentType && /^(?:image|text)/.test(this.contentType)) {
1328         try {
1329           let info = MimeService.getFromTypeAndExtension(this.contentType.split(';')[0], "");
1330           ext = info.primaryExtension;
1331         } catch (ex) {
1332           ext = '';
1333         }
1334       }
1335       else {
1336         name = this.fileName;
1337         ext = '';
1338       }
1339       let ref = this.referrer ? this.referrer.host.toString() : '';
1340      
1341       let curl = (uri.host + ((uripath=="") ? "" : (SYSTEMSLASH + uripath)));
1342      
1343       var replacements = {
1344         "name": name,
1345         "ext": ext,
1346         "text": description,
1347         "url": host,
1348         "subdirs": uripath,
1349         "flatsubdirs": uripath.replaceSlashes('_'),
1350         "refer": ref,
1351         "qstring": query,
1352         "curl": curl,
1353         "flatcurl": curl.replaceSlashes('_'),
1354         "num": Utils.formatNumber(this.numIstance),
1355         "hh": Utils.formatNumber(this.startDate.getHours(), 2),
1356         "mm": Utils.formatNumber(this.startDate.getMinutes(), 2),
1357         "ss": Utils.formatNumber(this.startDate.getSeconds(), 2),
1358         "d": Utils.formatNumber(this.startDate.getDate(), 2),
1359         "m": Utils.formatNumber(this.startDate.getMonth() + 1, 2),
1360         "y": String(this.startDate.getFullYear())
1361       }
1362       function replacer(type) {
1363         var t = type.substr(1, type.length - 2);
1364         if (t in replacements) {
1365           return replacements[t];
1366         }
1367         return type;
1368       }
1369      
1370       mask = mask.replace(/\*\w+\*/gi, replacer);
1371
1372       mask = mask.removeBadChars().removeFinalChar(".").trim().split(SYSTEMSLASH);
1373       let file = new FileFactory(this.pathName.addFinalSlash());
1374       while (mask.length) {
1375         file.append(mask.shift());
1376       }
1377       this._destinationName = file.leafName;
1378       this._destinationPath = file.parent.path;
1379     }
1380     catch(ex) {
1381       this._destinationName = this.fileName;
1382       this._destinationPath = this.pathName.addFinalSlash();
1383       Debug.log("rebuildDestination():", ex);
1384     }
1385     this._destinationNameFull = Utils.formatConflictName(
1386       this.destinationNameOverride ? this.destinationNameOverride : this._destinationName,
1387       this.conflicts
1388     );
1389     let file = new FileFactory(this.destinationPath);
1390     file.append(this.destinationName);
1391     this._destinationFile = file.path;
1392     this._icon = null;
1393   },
1394
1395   fail: function QI_fail(title, msg, state) {
1396     Debug.logString("failDownload invoked");
1397
1398     this.cancel(state);
1399
1400     Utils.playSound("error");
1401
1402     switch (Prefs.alertingSystem) {
1403       case 1:
1404         AlertService.show(title, msg, false);
1405         break;
1406       case 0:
1407         alert(msg);
1408         break;
1409     }
1410   },
1411
1412   cancel: function QI_cancel(message) {
1413     try {
1414       if (this.is(CANCELED)) {
1415         return;
1416       }
1417       if (this.is(COMPLETE)) {
1418         Dialog.completed--;
1419       }
1420       else if (this.is(RUNNING)) {
1421         this.pause();
1422       }
1423       this.state = CANCELED;     
1424       Debug.logString(this.fileName + ": canceled");
1425
1426       this.visitors = new VisitorManager();
1427
1428       if (message == "" || !message) {
1429         message = _("canceled");
1430       }
1431       this.status = message;
1432
1433
1434       this.removeTmpFile();
1435
1436       // gc
1437       this.chunks = [];
1438       this.totalSize = this.partialSize = 0;
1439       this.maxChunks = this.activeChunks = 0;
1440       this.conflicts = 0;
1441       this.resumable = true;
1442       this._autoRetries = 0;
1443
1444     } catch(ex) {
1445       Debug.log("cancel():", ex);
1446     }
1447   },
1448  
1449   removeTmpFile: function QI_removeTmpFile() {
1450     if (this.tmpFile.exists()) {
1451       try {
1452         this.tmpFile.remove(false);
1453       }
1454       catch (ex) {
1455         Debug.log("failed to remove tmpfile: " + this.tmpFile.path, ex);
1456       }
1457     }
1458     else {
1459       Debug.logString("tmpfile not found: " + this.tmpFile.path);
1460     }
1461   },
1462   sessionConnections: 0,
1463   _autoRetries: 0,
1464   _autoRetryTime: 0,
1465   markAutoRetry: function QI_markRetry() {
1466     if (!Prefs.autoRetryInterval || (Prefs.maxAutoRetries && Prefs.maxAutoRetries <= this._autoRetries)) {
1467        return;
1468     }
1469     this._autoRetryTime = Utils.getTimestamp();
1470     Debug.logString("marked auto-retry: " + d);
1471   },
1472   autoRetry: function QI_autoRetry() {
1473     if (!this._autoRetryTime || Utils.getTimestamp() - (Prefs.autoRetryInterval * 1000) < this._autoRetryTime) {
1474       return;
1475     }
1476
1477     this._autoRetryTime = 0;
1478     ++this._autoRetries;
1479     this.queue();
1480     Debug.logString("Requeued due to auto-retry: " + d);   
1481   },
1482   queue: function QI_queue() {
1483     this._autoRetryTime = 0;
1484     this.state = QUEUED;
1485     this.status = _("inqueue");
1486   },
1487   resumeDownload: function QI_resumeDownload() {
1488     Debug.logString("resumeDownload: " + this);
1489     function cleanChunks(d) {
1490       // merge finished chunks together, so that the scoreboard does not bloat that much
1491       for (let i = d.chunks.length - 2; i > -1; --i) {
1492         let c1 = d.chunks[i], c2 = d.chunks[i + 1];
1493         if (c1.complete && c2.complete) {
1494           c1.merge(c2);
1495           d.chunks.splice(i + 1, 1);
1496         }
1497       }
1498     }
1499     function downloadNewChunk(download, start, end, header) {
1500       var chunk = new Chunk(download, start, end);
1501       Debug.logString("started: " + chunk);
1502       download.chunks.push(chunk);
1503       download.chunks.sort(function(a,b) { return a.start - b.start; });
1504       downloadChunk(download, chunk, header);
1505       download.sessionConnctions = 0;
1506     }
1507     function downloadChunk(download, chunk, header) {
1508       chunk.running = true;
1509       download.state = RUNNING;
1510       Debug.logString("started: " + chunk);
1511       chunk.download = new Connection(download, chunk, header);
1512       ++download.activeChunks;
1513       ++download.sessionConnections;
1514     }
1515    
1516     cleanChunks(this);
1517
1518     try {
1519       if (this.maxChunks <= this.activeChunks) {
1520         return false;
1521       }
1522
1523       var rv = false;
1524
1525       // we didn't load up anything so let's start the main chunk (which will grab the info)
1526       if (this.chunks.length == 0) {
1527         downloadNewChunk(this, 0, 0, true);
1528         return false;
1529       }
1530
1531       // start some new chunks
1532       var paused = this.chunks.filter(
1533         function (chunk) {
1534           return !(chunk.running || chunk.complete);
1535         }
1536       );
1537       while (this.activeChunks < this.maxChunks) {
1538
1539         // restart paused chunks
1540         if (paused.length) {
1541           downloadChunk(this, paused.shift());
1542           rv = true;
1543           continue;
1544         }
1545        
1546         // find biggest chunk
1547         let biggest = null;
1548         this.chunks.forEach(
1549           function (chunk) {
1550             if (chunk.running && chunk.remainder > MIN_CHUNK_SIZE * 2) {
1551               if (!biggest || biggest.remainder < chunk.remainder) {
1552                 biggest = chunk;
1553               }
1554             }
1555           }
1556         );
1557
1558         // nothing found, break
1559         if (!biggest) {
1560           break;
1561         }
1562         var end = biggest.end;
1563         var bend = biggest.start + biggest.written + Math.floor(biggest.remainder / 2);
1564         biggest.end = bend;
1565         downloadNewChunk(this, biggest.end + 1, end);
1566         rv = true;
1567       }
1568
1569       return rv;
1570     }
1571     catch(ex) {
1572       Debug.log("resumeDownload():", ex);
1573     }
1574     return false;
1575   },
1576   dumpScoreboard: function QI_dumpScoreboard() {
1577     let scoreboard = '';
1578     let len = String(this.totalSize).length;
1579     this.chunks.forEach(
1580       function(c,i) {
1581         scoreboard += i
1582           + ": "
1583           + c
1584           + "\n";
1585       }
1586     );
1587     Debug.logString("scoreboard\n" + scoreboard);
1588   }, 
1589   toString: function() {
1590     return this.urlManager.usable;
1591   },
1592   toSource: function() {
1593     let e = {};
1594     [
1595       'fileName',
1596       'numIstance',
1597       'description',
1598       'resumable',
1599       'mask',
1600       'pathName',
1601       'compression',
1602       'maxChunks',
1603       'contentType',
1604       'conflicts',
1605     ].forEach(
1606       function(u) {
1607         e[u] = this[u];
1608       },
1609       this
1610     );
1611     if (this.hash) {
1612       e.hash = _atos(this.hash.sum);
1613       e.hashType = _atos(this.hash.type);
1614     }
1615     e.state = this.is(COMPLETE, CANCELED, FINISHING) ? this.state : PAUSED;
1616     if (this.destinationNameOverride) {
1617       this.destinationName = this.destinationNameOverride;
1618     }
1619     if (this.referrer) {
1620       e.referrer = this.referrer.spec;
1621     }
1622     // Store this so we can later resume.
1623     if (!this.is(CANCELED, COMPLETE) && this.partialSize) {
1624       e.tmpFile = this.tmpFile.path;
1625     }
1626     e.startDate = this.startDate.getTime();
1627
1628     e.urlManager = this.urlManager.toSource();
1629     e.visitors = this.visitors.toSource();
1630
1631     if (!this.resumable && !this.is(COMPLETE)) {
1632       e.totalSize = 0;
1633     }
1634     else {
1635       e.totalSize = this.totalSize;
1636     }
1637    
1638     e.chunks = [];
1639
1640     if (this.is(RUNNING, PAUSED, QUEUED) && this.resumable) {
1641       this.chunks.forEach(
1642         function(c) {
1643           e.chunks.push({start: c.start, end: c.end, written: c.safeBytes});
1644         }
1645       );
1646     }
1647     return e.toSource();
1648   }
1649 }
1650
1651 function Chunk(download, start, end, written) {
1652   // saveguard against null or strings and such
1653   this._written = written > 0 ? written : 0;
1654   this._buffered = 0;
1655   this._start = start;
1656   this._end = end;
1657   this.end = end;
1658   this._parent = download;
1659   this._sessionbytes = 0;
1660 }
1661
1662 Chunk.prototype = {
1663   running: false,
1664   get starter() {
1665     return this.end <= 0;
1666   },
1667   get start() {
1668     return this._start;
1669   },
1670   get end() {
1671     return this._end;
1672   },
1673   set end(nv) {
1674     this._end = nv;
1675     this._total = this._end - this._start + 1;
1676   },
1677   get total() {
1678     return this._total;
1679   },
1680   get written() {
1681     return this._written;
1682   },
1683   get safeBytes() {
1684     return this.written - this._buffered;
1685   },
1686   get remainder() {
1687     return this._total - this._written;
1688   },
1689   get complete() {
1690     if (this._end == -1) {
1691       return this.written != 0;
1692     }
1693     return this._total == this.written;
1694   },
1695   get parent() {
1696     return this._parent;
1697   },
1698   merge: function CH_merge(ch) {
1699     if (!this.complete && !ch.complete) {
1700       throw new Error("Cannot merge incomplete chunks this way!");
1701     }
1702     this.end = ch.end;
1703     this._written += ch._written;
1704   },
1705   open: function CH_open() {
1706     this._sessionBytes = 0;
1707     let file = this.parent.tmpFile.clone();
1708     if (!file.parent.exists()) {
1709       file.parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
1710       this.parent.invalidate();
1711     }
1712     let prealloc = !file.exists();
1713     let outStream = new FileOutputStream(file, 0x02 | 0x08, 0600, 0);
1714     let seekable = outStream.QueryInterface(Ci.nsISeekableStream);
1715     if (prealloc && this.parent.totalSize > 0) {
1716       try {
1717         seekable.seek(0x00, this.parent.totalSize);
1718         seekable.setEOF();
1719       }
1720       catch (ex) {
1721         // no-op
1722       }
1723     }
1724     seekable.seek(0x00, this.start + this.written);
1725     this._outStream = new BufferedOutputStream(outStream, CHUNK_BUFFER_SIZE);
1726   },
1727   close: function CH_close() {
1728     this.running = false;
1729     if (this._outStream) {
1730       this._outStream.flush();
1731       this._outStream.close();
1732       delete this._outStream;
1733     }
1734     this._buffered = 0;
1735     if (this.parent.is(CANCELED)) {
1736       this.parent.removeTmpFile();
1737     }
1738   },
1739   rollback: function CH_rollback() {
1740     if (!this._sessionBytes || this._sessionBytes > this._written) {
1741       return;
1742     }
1743     this._written -= this._sessionBytes;
1744     this._sessionBytes = 0;
1745   },
1746   cancel: function CH_cancel() {
1747     this.running = false;
1748     this.close();
1749     if (this.download) {
1750       this.download.cancel();
1751     }
1752   },
1753   _written: 0,
1754   _outStream: null,
1755   write: function CH_write(aInputStream, aCount) {
1756     try {
1757       if (!this._outStream) {
1758         this.open();
1759       }
1760       bytes = this.remainder;
1761       if (!this.total || aCount < bytes) {
1762         bytes = aCount;
1763       }
1764       if (!bytes) {
1765         return 0;
1766       }
1767       if (bytes < 0) {
1768         throw new Exception("bytes negative");
1769       }
1770       // we're using nsIFileOutputStream
1771       if (this._outStream.writeFrom(aInputStream, bytes) != bytes) {
1772         throw ("chunks::write: read/write count mismatch!");
1773       }
1774       this._written += bytes;
1775       this._sessionBytes += bytes;
1776       this._buffered = Math.min(CHUNK_BUFFER_SIZE, this._buffered + bytes);
1777
1778       this.parent.timeLastProgress = Utils.getTimestamp();
1779
1780       return bytes;
1781     }
1782     catch (ex) {
1783       Debug.log('write: ' + this.parent.tmpFile.path, ex);
1784       throw ex;
1785     }
1786     return 0;
1787   },
1788   toString: function() {
1789     let len = this.parent.totalSize ? String(this.parent.totalSize).length  : 10;
1790     return Utils.formatNumber(this.start, len)
1791       + "/"
1792       + Utils.formatNumber(this.end, len)
1793       + "/"
1794       + Utils.formatNumber(this.total, len)
1795       + " running:"
1796       + this.running
1797       + " written/remain:"
1798       + Utils.formatNumber(this.written, len)
1799       + "/"
1800       + Utils.formatNumber(this.remainder, len);
1801   }
1802 }
1803
1804 function Connection(d, c, getInfo) {
1805
1806   this.d = d;
1807   this.c = c;
1808   this.isInfoGetter = getInfo;
1809   this.url = d.urlManager.getURL();
1810   var referrer = d.referrer;
1811   Debug.logString("starting: " + this.url.url);
1812
1813   this._chan = IOService.newChannelFromURI(this.url.url.toURL());
1814   var r = Ci.nsIRequest;
1815   this._chan.loadFlags = r.LOAD_NORMAL | r.LOAD_BYPASS_CACHE;
1816   this._chan.notificationCallbacks = this;
1817   try {
1818     var encodedChannel = this._chan.QueryInterface(Ci.nsIEncodedChannel);
1819     encodedChannel.applyConversion = false;
1820   }
1821   catch (ex) {
1822     // no-op
1823   }
1824   try {
1825     var http = this._chan.QueryInterface(Ci.nsIHttpChannel);
1826     if (c.start + c.written > 0) {
1827       http.setRequestHeader('Range', 'bytes=' + (c.start + c.written) + "-", false);
1828     }
1829     if (referrer instanceof Ci.nsIURI) {
1830       http.referrer = referrer;
1831     }
1832     http.setRequestHeader('Keep-Alive', '', false);
1833     http.setRequestHeader('Connection', 'close', false);
1834   }
1835   catch (ex) {
1836
1837   }
1838   this.c.running = true;
1839   this._chan.asyncOpen(this, null);
1840 }
1841 Connection.prototype = {
1842   _interfaces: [
1843     Ci.nsISupports,
1844     Ci.nsISupportsWeakReference,
1845     Ci.nsIWeakReference,
1846     Ci.nsICancelable,
1847     Ci.nsIInterfaceRequestor,
1848     Ci.nsIStreamListener,
1849     Ci.nsIRequestObserver,
1850     Ci.nsIProgressEventSink,
1851     Ci.nsIChannelEventSink,
1852     Ci.nsIAuthPrompt,
1853     Ci.nsIFTPEventSink
1854   ],
1855  
1856   cantCount: false,
1857
1858   QueryInterface: function DL_QI(iid) {
1859       if (this._interfaces.some(function(i) { return iid.equals(i); })) {
1860         return this;
1861       }
1862       throw Components.results.NS_ERROR_NO_INTERFACE;
1863   },
1864   // nsISupportsWeakReference
1865   GetWeakReference: function DL_GWR() {
1866     return this;
1867   },
1868   // nsIWeakReference
1869   QueryReferent: function DL_QR(uuid) {
1870     return this.QueryInterface(uuid);
1871   },
1872   // nsICancelable
1873   cancel: function DL_cancel(aReason) {
1874     try {
1875       if (this._closed) {
1876         return;
1877       }
1878       Debug.logString("cancel");
1879       if (!aReason) {
1880         aReason = NS_ERROR_BINDING_ABORTED;
1881       }
1882       this._chan.cancel(aReason);
1883       this._closed = true;
1884     }
1885     catch (ex) {
1886       Debug.log("cancel", ex);
1887     }
1888   },
1889   // nsIInterfaceRequestor
1890   getInterface: function DL_getInterface(iid) {
1891     try {
1892       return this.QueryInterface(iid);
1893     }
1894     catch (ex) {
1895       Debug.log("interface not implemented: " + iid, ex);
1896       throw ex;
1897     }
1898   },
1899   get authPrompter() {
1900     try {
1901       return WindowWatcherService.getNewAuthPrompter(null)
1902         .QueryInterface(Ci.nsIAuthPrompt);
1903     }
1904     catch (ex) {
1905       Debug.log("authPrompter", ex);
1906       throw ex;
1907     }
1908   },
1909   // nsIAuthPrompt
1910   prompt: function DL_prompt(aDialogTitle, aText, aPasswordRealm, aSavePassword, aDefaultText, aResult) {
1911     return this.authPrompter.prompt(
1912       aDialogTitle,
1913       aText,
1914       aPasswordRealm,
1915       aSavePassword,
1916       aDefaultText,
1917       aResult
1918     );
1919   },
1920
1921   promptUsernameAndPassword: function DL_promptUaP(aDialogTitle, aText, aPasswordRealm, aSavePassword, aUser, aPwd) {
1922     return this.authPrompter.promptUsernameAndPassword(
1923       aDialogTitle,
1924       aText,
1925       aPasswordRealm,
1926       aSavePassword,
1927       aUser,
1928       aPwd
1929     );
1930   },
1931   promptPassword: function DL_promptPassword(aDialogTitle, aText, aPasswordRealm, aSavePassword, aPwd) {
1932     return this.authPrompter.promptPassword(
1933       aDialogTitle,
1934       aText,
1935       aPasswordRealm,
1936       aSavePassword,
1937       aPwd
1938     );
1939   },
1940  
1941   // nsIChannelEventSink
1942   onChannelRedirect: function DL_onChannelRedirect(oldChannel, newChannel, flags) {
1943     if (!this.isInfoGetter) {
1944       return;
1945     }
1946     try {
1947       this._chan == newChannel;
1948       this.url.url = newChannel.URI.spec;
1949       this.d.fileName = this.url.usable.getUsableFileName();
1950     }
1951     catch (ex) {
1952       // no-op
1953     }
1954   },
1955  
1956   // nsIStreamListener
1957   onDataAvailable: function DL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) {
1958     if (this._closed) {
1959       throw 0x804b0002; // NS_BINDING_ABORTED;
1960     }
1961     try {
1962       // we want to kill ftp chans as well which do not seem to respond to cancel correctly.
1963       if (!this.c.write(aInputStream, aCount)) {
1964         // we already got what we wanted
1965         this.cancel();
1966       }
1967     }
1968     catch (ex) {
1969       Debug.log('onDataAvailable', ex);
1970       this.d.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
1971     }
1972   },
1973  
1974   OnFTPControlLog: function(server, msg) {},
1975
1976   handleError: function DL_handleError() {
1977     let c = this.c;
1978     let d = this.d;
1979    
1980     c.cancel();
1981     d.dumpScoreboard();
1982     if (d.chunks.indexOf(c) == -1) {
1983       // already killed;
1984       return true;
1985     }
1986
1987     Debug.logString("handleError: problem found; trying to recover");
1988    
1989     if (d.urlManager.markBad(this.url)) {
1990       Debug.logString("handleError: fresh urls available, kill this one and use another!");
1991       d.timeLastProgress = Utils.getTimestamp();
1992       return true;
1993     }
1994    
1995     Debug.logString("affected: " + c);
1996    
1997     let max = -1, found = -1;
1998     for (let i = 0; i < d.chunks.length; ++i) {
1999       let cmp = d.chunks[i];
2000       if (cmp.start < c.start && cmp.start > max) {
2001         found = i;
2002         max = cmp.start;
2003       }
2004     }
2005     if (found > -1) {
2006       Debug.logString("handleError: found joinable chunk; recovering suceeded, chunk: " + found);
2007       d.chunks[found].end = c.end;
2008       if (--d.maxChunks == 1) {
2009         //d.resumable = false;
2010       }
2011       d.chunks = d.chunks.filter(function(ch) { return ch != c; });
2012       d.chunks.sort(function(a,b) { return a.start - b.start; });
2013      
2014       // check for overlapping ranges we might have created
2015       // otherwise we'll receive a size mismatch
2016       // this means that we're gonna redownload an already finished chunk...
2017       for (let i = d.chunks.length - 2; i > -1; --i) {
2018         let c1 = d.chunks[i], c2 = d.chunks[i + 1];
2019         if (c1.end >= c2.end) {
2020           if (c2.running) {
2021             // should never ever happen :p
2022             d.dumpScoreboard();
2023             Debug.logString("overlapping:\n" + c1 + "\n" + c2);
2024             d.fail("Internal error", "Please notify the developers that there were 'overlapping chunks'!", "Internal error (please report)");
2025             return false;
2026           }
2027           d.chunks.splice(i + 1, 1);
2028         }
2029       }
2030       let ac = 0;
2031       d.chunks.forEach(function(c) { if (c.running) { ++ac; }});
2032       d.activeChunks = ac;
2033       c.close();
2034      
2035       SessionManager.save(d);
2036       d.dumpScoreboard();
2037       return true;
2038     }
2039     return false;
2040   },
2041   handleHttp: function DL_handleHttp(aChannel) {
2042     let c = this.c;
2043     let d = this.d;
2044    
2045     let code = 0, status = 'Server returned nothing';
2046     try {
2047       code = aChannel.responseStatus;
2048       status = aChannel.responseStatusText;
2049     }
2050     catch (ex) {
2051       return true;
2052     }
2053      
2054     if (code >= 400) {
2055       if (!this.handleError()) {
2056         Debug.log("handleError: Cannot recover from problem!", code);
2057         if ([401, 402, 407, 500, 502, 503, 504].indexOf(code) != -1) {
2058           Debug.log("we got temp failure!", code);
2059           d.pause();
2060           d.markAutoRetry();
2061           d.status = code >= 500 ? _('temperror') : _('autherror');
2062         }
2063         else if (code == 450) {
2064           d.fail(
2065             _('pcerrortitle'),
2066             _('pcerrortext'),
2067             _('pcerrortitle')
2068           );
2069         }
2070         else {
2071           var file = d.fileName.length > 50 ? d.fileName.substring(0, 50) + "..." : d.fileName;
2072           code = Utils.formatNumber(code, 3);
2073           d.fail(
2074             _("error", [code]),
2075             _("failed", [file]) + " " + _("sra", [code]) + ": " + status,
2076             _("error", [code])
2077           );
2078         }
2079         // any data that we got over this channel should be considered "corrupt"
2080         c.rollback();
2081         SessionManager.save(d);
2082       }
2083       return false;
2084     }
2085
2086     // not partial content altough we are multi-chunk
2087     if (code != 206 && !this.isInfoGetter) {
2088       Debug.log(d + ": Server returned a " + aChannel.responseStatus + " response instead of 206", this.isInfoGetter);
2089      
2090       d.resumable = false;
2091
2092       if (!this.handleError()) {
2093         vis = {value: '', visitHeader: function(a,b) { this.value += a + ': ' + b + "\n"; }};
2094         aChannel.visitRequestHeaders(vis);
2095         Debug.logString("Request Headers\n\n" + vis.value);
2096         vis.value = '';
2097         aChannel.visitResponseHeaders(vis);
2098         Debug.logString("Response Headers\n\n" + vis.value);
2099         d.cancel();
2100         d.resumable = false;
2101         d.safeRetry();
2102         return false;
2103       }
2104     }
2105
2106     var visitor = null;
2107     try {
2108       visitor = d.visitors.visit(aChannel);
2109     }
2110     catch (ex) {
2111       Debug.log("header failed! " + d, ex);
2112       // restart download from the beginning
2113       d.cancel();
2114       d.resumable = false;
2115       d.safeRetry();
2116       return false;
2117     }
2118    
2119     if (!this.isInfoGetter) {
2120       return false;
2121     }
2122
2123     if (visitor.type) {
2124       d.contentType = visitor.type;
2125     }
2126
2127     // compression?
2128     if (['gzip', 'deflate'].indexOf(visitor.encoding) != -1 && !d.contentType.match(/gzip/i) && !d.fileName.match(/\.gz$/i)) {
2129       d.compression = visitor.encoding;
2130     }
2131     else {
2132       d.compression = null;
2133     }
2134
2135     // accept range
2136     d.resumable &= visitor.acceptRanges;
2137
2138     if (visitor.type && visitor.type.search(/application\/metalink\+xml/) != -1) {
2139       d.isMetalink = true;
2140       d.resumable = false;
2141     }
2142
2143     if (visitor.contentlength > 0) {
2144       d.totalSize = visitor.contentlength;
2145     } else {
2146       d.totalSize = 0;
2147     }
2148    
2149     if (visitor.fileName && visitor.fileName.length > 0) {
2150       // if content disposition hasn't an extension we use extension of URL
2151       let newName = visitor.fileName;
2152       let ext = this.url.usable.getExtension();
2153       if (visitor.fileName.lastIndexOf('.') == -1 && ext) {
2154         newName += '.' + ext;
2155       }
2156       let charset = visitor.overrideCharset ? visitor.overrideCharset : this.url.charset;
2157       d.fileName = DTA_URLhelpers.decodeCharset(newName, charset).getUsableFileName();
2158     }
2159
2160     return false;
2161   },
2162  
2163   // Generic handler for now :p
2164   handleFtp: function  DL_handleFtp(aChannel) {
2165     return this.handleGeneric(aChannel);
2166   },
2167  
2168   handleGeneric: function DL_handleGeneric(aChannel) {
2169     var c = this.c;
2170     var d = this.d;
2171    
2172     // hack: determine if we are a multi-part chunk,
2173     // if so something bad happened, 'cause we aren't supposed to be multi-part
2174     if (c.start != 0 && d.is(RUNNING)) {
2175       if (!this.handleError()) {
2176         Debug.log(d + ": Server error or disconnection", "(type 1)");
2177         d.status = _("servererror");
2178         d.markAutoRetry();
2179         d.pause();
2180       }
2181       return false;
2182     }     
2183      
2184     // try to get the size anyway ;)
2185     try {
2186       let pb = aChannel.QueryInterface(Ci.nsIPropertyBag2);
2187       d.totalSize = Math.max(pb.getPropertyAsInt64('content-length'), 0);
2188     }
2189     catch (ex) {
2190       try {
2191         d.totalSize = Math.max(aChannel.contentLength, 0);
2192       }
2193       catch (ex) {
2194         d.totalSize = 0;
2195       }
2196     }
2197     d.resumable = false;
2198     return false;
2199   },
2200  
2201   //nsIRequestObserver,
2202   _supportedChannels: [
2203     {i:Ci.nsIHttpChannel, f:'handleHttp'},
2204     {i:Ci.nsIFTPChannel, f:'handleFtp'},
2205     {i:Ci.nsIChannel, f:'handleGeneric'}
2206   ],
2207   onStartRequest: function DL_onStartRequest(aRequest, aContext) {
2208     let c = this.c;
2209     let d = this.d;
2210     Debug.logString('StartRequest: ' + c);
2211  
2212     this.started = true;
2213     try {
2214       for (let i = 0, e = this._supportedChannels.length; i < e; ++i) {
2215         let sc = this._supportedChannels[i];
2216         let chan = null;
2217         try {
2218           chan = aRequest.QueryInterface(sc.i);
2219         }
2220         catch (ex) {
2221           continue;
2222         }
2223         if (chan) {
2224           if ((this.rexamine = this[sc.f](chan))) {
2225              return;
2226           }
2227           break;
2228         }         
2229       }
2230
2231       if (this.isInfoGetter) {
2232         // Checks for available disk space.
2233         
2234         if (d.fileName.getExtension() == 'metalink') {
2235           d.isMetalink = true;
2236           d.resumable = true;
2237         }       
2238        
2239         var tsd = d.totalSize;
2240         try {
2241           if (tsd) {
2242             let tmp = Prefs.tempLocation, vtmp = 0;
2243             if (tmp) {
2244               vtmp = Utils.validateDir(tmp);
2245               if (!vtmp && Utils.getFreeDisk(vtmp) < tsd) {
2246                 d.fail(_("ndsa"), _("spacetemp"), _("freespace"));
2247                 return;
2248               }
2249             }
2250             let realDest = Utils.validateDir(d.destinationPath);
2251             if (!realDest) {
2252               throw new Error("invalid destination folder");
2253             }
2254             var nsd = Utils.getFreeDisk(realDest);
2255             // Same save path or same disk (we assume that tmp.avail == dst.avail means same disk)
2256             // simply moving should succeed
2257             if (d.compression && (!tmp || Utils.getFreeDisk(vtmp) == nsd)) {
2258               // we cannot know how much space we will consume after decompressing.
2259               // so we assume factor 1.0 for the compressed and factor 1.5 for the decompressed file.
2260               tsd *= 2.5;
2261             }
2262             if (nsd < tsd) {
2263               Debug.logString("nsd: " +  nsd + ", tsd: " + tsd);
2264               d.fail(_("ndsa"), _("spacedir"), _("freespace"));
2265               return;
2266             }
2267           }
2268         }
2269         catch (ex) {
2270           Debug.log("size check threw", ex);
2271           d.fail(_("accesserror"), _("permissions") + " " + _("destpath") + ". " + _("checkperm"), _("accesserror"));
2272           return;
2273         }
2274        
2275         if (!d.totalSize) {
2276           d.resumable = false;         
2277           this.cantCount = true;
2278         }
2279         if (!d.resumable) {
2280           d.maxChunks = 1;
2281         }
2282         c.end = d.totalSize - 1;
2283         delete this.isInfoGetter;
2284        
2285         // Explicitly trigger rebuildDestination here, as we might have received
2286         // a html content type and need to rewrite the file
2287         d.rebuildDestination();
2288         ConflictManager.resolve(d);
2289       }
2290      
2291       if (d.resumable && !d.is(CANCELED)) {
2292         d.resumeDownload();
2293       }
2294     }
2295     catch (ex) {
2296       Debug.log("onStartRequest", ex);
2297     }
2298   },
2299   onStopRequest: function DL_onStopRequest(aRequest, aContext, aStatusCode) {
2300     try {
2301       Debug.logString('StopRequest');
2302     }
2303     catch (ex) {
2304       return;
2305     }
2306    
2307     // shortcuts
2308     let c = this.c;
2309     let d = this.d;
2310     c.close();
2311    
2312     if (d.chunks.indexOf(c) == -1) {
2313       return;
2314     }
2315
2316     // update flags and counters
2317     d.refreshPartialSize();
2318     --d.activeChunks;
2319
2320     // check if we're complete now
2321     if (d.is(RUNNING) && d.chunks.every(function(e) { return e.complete; })) {
2322       if (!d.resumeDownload()) {
2323         d.state = FINISHING;
2324         Debug.logString(d + ": Download is complete!");
2325         d.finishDownload();
2326         return;
2327       }
2328     }
2329
2330     if (c.starter && -1 != [
2331       NS_ERROR_CONNECTION_REFUSED,
2332       NS_ERROR_UNKNOWN_HOST,
2333       NS_ERROR_NET_TIMEOUT,
2334       NS_ERROR_NET_RESET
2335     ].indexOf(aStatusCode)) {
2336       Debug.log(d + ": Server error or disconnection", "(type 3)");
2337       d.pause();
2338       d.status = _("servererror");
2339       d.markAutoRetry();       
2340       return;
2341     }   
2342
2343     // routine for normal chunk
2344     Debug.logString(d + ": Chunk " + c.start + "-" + c.end + " finished.");
2345    
2346     // rude way to determine disconnection: if connection is closed before download is started we assume a server error/disconnection
2347     if (c.starter && d.is(RUNNING)) {
2348       if (!d.urlManager.markBad(this.url)) {
2349         Debug.log(d + ": Server error or disconnection", "(type 2)");
2350         d.pause();
2351         d.status = _("servererror");
2352         d.markAutoRetry();       
2353       }
2354       else {
2355         Debug.log("caught bad server", d.toString());
2356         d.cancel();
2357         d.safeRetry();
2358       }
2359       return;     
2360     }
2361
2362     if (!d.is(PAUSED, CANCELED, FINISHING) && d.chunks.length == 1 && d.chunks[0] == c) {
2363       if (d.resumable) {
2364         d.pause();
2365         d.markAutoRetry();
2366         d.status = _('errmismatchtitle');
2367       }
2368       else {
2369         d.fail(
2370           _('errmismatchtitle'),
2371           _('errmismatchtext', [d.partialSize, d.totalSize]),
2372           _('errmismatchtitle')
2373         );
2374       }
2375       return;     
2376     }
2377     if (!d.is(PAUSED, CANCELED)) {
2378       d.resumeDownload();
2379     }
2380   },
2381
2382   // nsIProgressEventSink
2383   onProgress: function DL_onProgress(aRequest, aContext, aProgress, aProgressMax) {
2384     try {
2385       // shortcuts
2386       let c = this.c;
2387       let d = this.d;
2388      
2389       if (this.reexamine) {
2390         Debug.logString(d + ": reexamine");
2391         this.onStartRequest(aRequest, aContext);
2392         if (this.reexamine) {
2393           return;
2394         }
2395       }
2396
2397       // update download tree row
2398       if (d.is(RUNNING)) {
2399         d.refreshPartialSize();
2400
2401         if (!this.resumable && d.totalSize) {
2402           // basic integrity check
2403           if (d.partialSize > d.totalSize) {
2404             d.dumpScoreboard();
2405             Debug.logString(d + ": partialSize > totalSize" + "(" + d.partialSize + "/" + d.totalSize + "/" + ( d.partialSize - d.totalSize) + ")");
2406             d.fail(
2407               _('errmismatchtitle'),
2408               _('errmismatchtext', [d.partialSize, d.totalSize]),
2409               _('errmismatchtitle')
2410             );
2411             return;
2412           }
2413         }
2414         else {
2415           d.status = _("downloading");
2416         }
2417       }
2418     }
2419     catch(ex) {
2420       Debug.log("onProgressChange():", e);
2421     }
2422   },
2423   onStatus: function  DL_onStatus(aRequest, aContext, aStatus, aStatusArg) {}
2424 };
2425
2426 function startDownloads(start, downloads) {
2427
2428   var numbefore = Tree.rowCount - 1;
2429   const DESCS = ['description', 'ultDescription'];
2430  
2431   let g = downloads;
2432   if ('length' in downloads) {
2433     g = function() {
2434        for (let i = 0, e = downloads.length; i < e; ++i) {
2435         yield downloads[i];
2436        }
2437     }();
2438   }
2439
2440   let added = 0;
2441   let removeableTabs = {};
2442   Tree.beginUpdate();
2443   SessionManager.beginUpdate();
2444   for (let e in g) {
2445     e.dirSave.addFinalSlash();
2446
2447     var desc = "";
2448     DESCS.some(
2449       function(i) {
2450         if (typeof(e[i]) == 'string' && e[i].length) {
2451           desc = e.description;
2452           return true;
2453         }
2454         return false;
2455       }
2456     );
2457
2458     var d = new QueueItem(
2459       e.url,
2460       e.dirSave,
2461       e.numIstance,
2462       desc,
2463       e.mask,
2464       e.referrer
2465     );
2466     if (e.url.hash) {
2467       d.hash = e.url.hash;
2468     }
2469     else if (e.hash) {
2470       d.hash = e.hash;
2471     }
2472     else {
2473       d.hash = null; // to initialize prettyHash
2474     }
2475     d.state = start ? QUEUED : PAUSED;
2476     if (d.is(QUEUED)) {
2477       d.status = _('inqueue');
2478     }
2479     else {
2480       d.status = _('paused');
2481     }
2482     Tree.add(d);
2483     SessionManager.save(d);
2484     ++added;
2485   }
2486   Tree.endUpdate();
2487   SessionManager.endUpdate();
2488
2489   var boxobject = Tree._box;
2490   boxobject.QueryInterface(Ci.nsITreeBoxObject);
2491   if (added <= boxobject.getPageLength()) {
2492     boxobject.scrollToRow(Tree.rowCount - boxobject.getPageLength());
2493   }
2494   else {
2495     boxobject.scrollToRow(numbefore);
2496   }
2497 }
2498 const FileOutputStream = Components.Constructor(
2499   '@mozilla.org/network/file-output-stream;1',
2500   'nsIFileOutputStream',
2501   'init'
2502 );
2503
2504 var ConflictManager = {
2505   _items: [],
2506   resolve: function CM_resolve(download, reentry) {
2507     if (!this._check(download)) {
2508       if (reentry) {
2509         download[reentry]();
2510       }
2511       return;
2512     }
2513     for (let i = 0; i < this._items.length; ++i) {
2514       if (this._items[i].download == download) {
2515         Debug.logString("conflict resolution updated to: " + reentry);
2516        
2517         this._items[i].reentry = reentry;
2518         return;
2519       }
2520     }
2521     Debug.logString("conflict resolution queued to: " + reentry);
2522     this._items.push({download: download, reentry: reentry});
2523     this._process();
2524   },
2525   _check: function CM__check(download) {
2526     let dest = new FileFactory(download.destinationFile);
2527     let sn = false;
2528     if (download.is(RUNNING)) {
2529       sn = Dialog.checkSameName(download, download.destinationFile);
2530     }
2531     Debug.logString("conflict check: " + sn + "/" + dest.exists() + " for " + download.destinationFile);
2532     return dest.exists() || sn;
2533   },
2534   _process: function CM__process() {
2535     if (this._processing) {
2536       return;
2537     }
2538     let cur;
2539     while (this._items.length) {
2540       cur = this._items[0];
2541       if (!this._check(cur.download)) {
2542         if (reentry) {
2543           cur.download[reentry]();
2544         }
2545         this._items.shift();
2546         continue;
2547       }
2548       break;
2549     }
2550     if (!this._items.length) {
2551       return;
2552     }
2553  
2554     if (Prefs.conflictResolution != 3) {
2555       this._return(Prefs.conflictResolution);
2556       return;
2557     }
2558     if (this._sessionSetting) {
2559       this._return(this._sessionSetting);
2560       return;
2561     }
2562     if (cur.download.shouldOverwrite) {
2563       this._return(1);
2564       return;
2565     }
2566    
2567     this._computeConflicts(cur);
2568
2569     var options = {
2570       url: cur.download.urlManager.usable.cropCenter(45),
2571       fn: cur.download.destinationName.cropCenter(45),
2572       newDest: cur.newDest.cropCenter(45)
2573     };
2574    
2575     this._processing = true;
2576    
2577     window.openDialog(
2578       "chrome://dta/content/dta/manager/conflicts.xul",
2579       "_blank",
2580       "chrome,centerscreen,resizable=no,dialog,close=no,dependent",
2581       options, this
2582     );
2583   },
2584   _computeConflicts: function CM__computeConflicts(cur) {
2585     let download = cur.download;
2586     download.conflicts = 0;
2587     let basename = download.destinationName;
2588     let newDest = new FileFactory(download.destinationFile);
2589     let i = 1;
2590     for (;; ++i) {
2591       newDest.leafName = Utils.formatConflictName(basename, i);
2592       if (!newDest.exists() && (!download.is(RUNNING) || !Dialog.checkSameName(this, newDest.path))) {
2593         break;
2594       }
2595     }
2596     cur.newDest = newDest.leafName;
2597     cur.conflicts = i; 
2598   },
2599   _returnFromDialog: function CM__returnFromDialog(option, type) {
2600     if (type == 1) {
2601       this._sessionSetting = option;
2602     }
2603     if (type == 2) {
2604       Preferences.setDTA('conflictresolution', option);
2605     }   
2606     this._return(option);
2607   },
2608   _return: function CM__return(option) {
2609     let cur = this._items[0];
2610     switch (option) {
2611       /* rename */    case 0: this._computeConflicts(cur); cur.download.conflicts = cur.conflicts; break;
2612       /* overwrite */ case 1: cur.download.shouldOverwrite = true; break;
2613       /* skip */      default: cur.download.cancel(_('skipped')); break;
2614     }
2615     if (cur.reentry) {
2616       cur.download[cur.reentry]();
2617     }
2618     this._items.shift();
2619     this._processing = false;
2620     this._process();
2621   }
2622 };
Note: See TracBrowser for help on using the browser.