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

Revision 971, 66.3 kB (checked in by MaierMan, 2 years ago)

#752: Manage offline status better

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