/*-*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Roaming code. * * The Initial Developer of the Original Code is * Ben Bucksch of * Beonex * Portions created by the Initial Developer are Copyright (C) 2002-2004 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Netscape Editor team, esp. Charles Manske * ManyOne * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ /* Classes to transfer a list of files and to track the transfer progress. It does not provide any UI. You basically only need to create a Transfer object and call its transfer() method. The callbacks will then be notified of the progress and possible errors. In the callbacks, you can ask about the status (incl. errors) of the files by reading the properties of the Transfer object (see Transfer.dump() and TransferFile.dump() for a short explanation). The functions in utility.js will help you with interpreting and displaying possible errors. The initial version/concept was stolen from Composer's publishing functionality, but heavily modified (probably almost all lines changed). */ const nsIChannel = Components.interfaces.nsIChannel; /* Passes parameters for transfer. @param download Bool download or upload @param serial Bool true: transfer one file after the other false: all files at once (parallel) @param localDir String file:-URL of the local dir, where the profile files are stored @param remoteDir String URL of the dir on the server, where the profile files are stored @param password String Password for server in cleartext @param savepw Bool Should password be stored by def. when asked? @param files Array of Objects with properties filename String path relative to localDir/remoteDir, plus filename (incl. possible extension) mimetype String undefined, if unknown size int in bytes. undefined, if unknown @param progessCallback function(file) to be called when the progress of a file transfer changed. can be used to e.g. update the UI. parameter file is an entry in the files array of this object, see makeFileVar() and [this.]dump(). @param finishedCallback function(bool success) to be called when we're done. |success|, if it seems that all files have been transferred successfully and completely. @throws e.result==kMalformedURI bad base URI @throws e.prop=="NoUsername" no username given */ function Transfer(download, serial, localDir, remoteDir, password, savepw, files, finishedCallback, progressCallback) { // from caller this.download = download; this.serial = serial; this.localDir = localDir; this.remoteDir = remoteDir; var uri = GetIOService().newURI(remoteDir, null, null); // this will throw an exception, if the user entered a bad URI. // catch in caller this.username = uri.username; if (!this.username || this.username == "") throw {prop:"NoUsername"}; this.password = password; if (!savepw) savepw = false; this.savePassword = savepw; //savepw is when the user checked the checkbox this.saveLogin = false; //savelogin is when we will need to write to disk this.sitename = uri.host; this.files = new Array(); for (var i = 0, l = files.length; i < l; i++) { this.files[i] = new TransferFile(this, i, files[i].filename, files[i].mimetype, files[i].size); } // internal management this.cancelled = false; // user pressed cancel. Don't proceed. // progress this.progressSizeAll = 0; for (var i = 0, l = this.files.length; i < l; i++) this.progressSizeAll += this.files[i].size; // We fixed up this.files[].size, so files != this.files this.progressSizeCompletedFiles = 0; /* Cumulated size of the files which have been *completely* (not partially) transferred. Used to speed up processing of incremental updates. */ // callbacks this.finishedCallbacks = new Array(); this.progressCallbacks = new Array(); if (finishedCallback) this.finishedCallbacks.push(finishedCallback); if (progressCallback) this.progressCallbacks.push(progressCallback); /* I'm using arrays here, to allow the caller to hook up additional callbacks. */ } Transfer.prototype = { /* Called when all files finished @param success bool no file failed */ done : function(success) { //ddump("Done(" + (success ? "without" : "with") + " errors)"); for (var i = 0, l = this.finishedCallbacks.length; i < l; i++) this.finishedCallbacks[i](success); /* The close dialog stuff (when all files are done successfully) is UI and thus in sroamingProgress.js, called from SetProgressStatus(), which should already be called. */ }, /* Initiate transfer of files Note: when this function returns, the transfer is most likely *not* finished yet. @throws "transfer failed" (from transferFile) */ transfer : function() { //ddump("starting transfer"); this.dump(); if (this.files.length == 0) this.done(true); if (this.serial) { this.nextFile(); } else { for (var i = 0, l = this.files.length; i < l; i++) this.transferFile(i); } }, /* In serial mode, start the download of the next file, because all previous downloads are finished. @return 1: there are still files busy (something went wrong) 2: transfer of next file started 3: all files were already done, nothing left to do. @throws "transfer failed" (from transferFile) */ nextFile : function() { // check that there are no downloads in progress for (var i = 0, l = this.files.length; i < l; i++) { if (this.files[i].status == "busy") { dumpError("Programming error: nextFile() called, although there is " + "still a download in progress."); this.dump(); //return 1; } } // all halted? if (this.cancelled) return 3; // find the next pending one for (var i = 0, l = this.files.length; i < l; i++) { if (this.files[i].status == "pending") { this.transferFile(i); return 2; } } return 3; }, /* Transfer that file. Implements the actual upload/download. Independent of serial/parallel (caller is responsible for that). @param id index of this.files array of the file to be transferred @throws "transfer failed" */ transferFile : function(id) { var file = this.files[id]; //ddump("TransferFile(" + id + ")"); try { var ioserv = GetIOService(); var baseURL = ioserv.newURI(this.remoteDir, null, null); if (this.savePassword && this.password && this.password != "") baseURL.password = this.password; var localURL = ioserv.newURI(this.localDir, null, null); var channel = ioserv.newChannel(file.filename, null, baseURL); file.channel = channel; var progress = new TransferProgressListener(this, id); file.progressListener = progress; channel.notificationCallbacks = progress; //ddumpCont("Trying to "+ (this.download ? "download from" : "upload to")); //ddumpCont(" remote URL <" + channel.URI.spec + "> "); if (this.download) { var fos = Components .classes["@mozilla.org/network/file-output-stream;1"] .createInstance(Components.interfaces.nsIFileOutputStream); var bos = Components .classes["@mozilla.org/network/buffered-output-stream;1"] .createInstance(Components.interfaces .nsIBufferedOutputStream); var ssl = Components .classes["@mozilla.org/network/simple-stream-listener;1"] .createInstance(Components.interfaces .nsISimpleStreamListener); /* Download to temporary local location first, in case something goes wrong, e.g. failed auth (in that case, necko overwrites the file with nothing! :-( ). Moved to final location in fileFinished(). */ var lf = ioserv.newURI(file.filename + ".tmp", null, localURL) .QueryInterface(Components.interfaces.nsIFileURL) .file.clone(); file.localFile = lf; //ddump("to local file " + lf.path); try { fos.init(lf, -1, -1, 0);//odd params from NS_NewLocalFileOutputStream } catch(e) { var dir = lf.parent; if (e.result == kFileNotFound // we get this error, if the directory does no exist yet locally && dir && !dir.exists()) { //ddumpCont("Creating dir " + dir.path + " ... "); dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755); // also creates all higher-level dirs, if necessary //ddump("done (hopefully)."); fos.init(lf, -1, -1, 0); // try again } else throw e; // pass on all other errors } bos.init(fos, 32768); ssl.init(bos, progress); file.fos = fos; // for flush after finished file.bos = bos; // dito channel.asyncOpen(ssl, null); } else // upload { var uploadChannel = channel.QueryInterface( Components.interfaces.nsIUploadChannel); var lfURL = ioserv.newURI(file.filename, null, localURL); //ddump("from local file <" + lfURL.spec + ">"); var fileChannel = ioserv.newChannelFromURI(lfURL); var is = fileChannel.open(); // maybe async? WFM. var bis = Components .classes["@mozilla.org/network/buffered-input-stream;1"] .createInstance(Components.interfaces .nsIBufferedInputStream); bis.init(is, 32768); uploadChannel.setUploadStream(bis, file.mimetype, -1); /* I *must* use a mimetype here. Passing null will cause the HTTP protocol implementation to use POST instead of PUT. */ channel.asyncOpen(progress, null); } } catch(e) { if (e.result) // XPCOM error, we can report that to the user and we // partially even expect it (file not found in new profiles etc.) { //ddump("Transfer problem, will later report it to user:\n" + e); file.setStatus("failed", e.result, e.message); } else { //ddump("Unexpected (non-XPCOM) transfer problem:\n" + e); file.setStatus("failed", kErrorUnexpected, e); } } }, // called when an individual file transfer completed or failed fileFinished : function(filenr) { //ddump("file " + filenr + " finished"); var file = this.files[filenr]; // flush/close - only for download if (file.bos && file.fos) { file.bos.flush(); file.fos.flush(); file.bos.close(); file.fos.close(); } // move to final location if (this.download && file.localFile && file.localFile.exists()) { if (file.status == "done") file.localFile.moveTo(null, file.filename) else file.localFile.remove(false); } // check, if we're done with all files var done = true; var failed = false; for (var i = 0, l = this.files.length; i < l; i++) { var file = this.files[i]; if (file.status == "failed") failed = true; // |done| stays true else if (file.status != "done") // "failed" already tested above done = false; } if (done) this.done(!failed); else if (this.serial) this.nextFile(); }, // user wishes transfer to stop cancel : function() { //ddump("Canceling transfer"); this.cancelled = true; /* needed so that file.setStatus doesn't start a next transfer in serial mode */ // network stuff for (var i = 0, l = this.files.length; i < l; i++) { if (this.files[i].status != "done" && this.files[i].status != "failed") { this.files[i].setStatus("failed", kErrorAbort); var channel = this.files[i].channel; if (channel && channel.isPending) { channel.cancel(kErrorAbort); /* the above line sets all files in the UI to failed, but that does not close the dialog. */ } } } /* finishedCallbacks called by done(), which will be called once all files are finished/cancelled. */ }, /* Hack. We need to change the list of files (add or remove files) after the Transfer object was created. However, in that case, the internal bookkeeping stuff (filenr of TransferFile) won't match anymore, so this function fixes that. Note: If you add files, create them using new TransferFile(). If you remove files, do that best using splice or readd the remaining files to a new array. */ filesChanged : function() { for (var i = 0, l = this.files.length; i < l; i++) { this.files[i].filenr = i; } }, // Progress // functions dealing with tracking progress // not sure, if that should be at the progress listener progress : 0.0, // Gives the overall transfer progress of all files (0..1) progressCalculate : true, // if false, don't update this.progress /* The progress listener calls this function to notify it that the transfer progress of a certain file changed. This function will update the file's progress this.files[].progress and the overall progress this.progress. It will also call the owner's callbacks. SLOW! :-( @param filenr int index of the file whose progress changed @param aProgress float 0..1 progress of that file */ progressChanged : function(filenr, aProgress) { if (!this.progressCalculate) return; //ddump("Transfer.progressChanged(" + filenr + ", " + aProgress + ")"); var file = this.files[filenr]; file.progress = aProgress; if (this.progressSizeAll == 0) // undetermined mode return; // avoid too many updates if (file.progress - file.progressPrevious < 0.01) return; else file.progressPrevious = file.progress; // calculate progress var progressSize = 0; if (file.status == "done") { //ddump(" file completed mode"); //ddump(" progressSizeAll: " + this.progressSizeAll); //ddumpCont(" progressSizeCompletedFiles before: "); //ddump(this.progressSizeCompletedFiles); this.progressSizeCompletedFiles = 0; for (var i = 0, l = this.files.length; i < l; i++) { var cur = this.files[i]; if (cur.status == "done") this.progressSizeCompletedFiles += cur.size; } progressSize = this.progressSizeCompletedFiles; //ddumpCont(" progressSizeCompletedFiles now: "); //ddump(this.progressSizeCompletedFiles); //ddump(" progressSize: " + progressSize); } else { /* It's probably just an update during the file download - that happens often, so performance matters. */ if (this.serial) { //ddump(" update serial mode"); //ddump(" progressSizeAll: " + this.progressSizeAll); //ddump(" progressSizeCompletedFiles: "+this.progressSizeCompletedFiles); //ddump(" files[filenr].size: " + file.size); //ddump(" files[filenr].progress: " + file.progress); progressSize = this.progressSizeCompletedFiles + file.size * file.progress; //ddump(" progressSize: " + progressSize); } else { //ddump(" update parallel mode"); //ddump(" progressSizeCompletedFiles: "+this.progressSizeCompletedFiles); progressSize = this.progressSizeCompletedFiles; for (var i = 0, l = this.files.length; i < l; i++) { var cur = this.files[i]; if (cur.status == "busy") progressSize += cur.size * cur.progress; } //ddump(" progressSize: " + progressSize); } } this.progress = progressSize / this.progressSizeAll; // I protected at the very beginning against this.progressSizeAll == 0 for (var i = 0, l = this.progressCallbacks.length; i < l; i++) this.progressCallbacks[i](filenr); }, // functions dealing with conflict-(resolution), i.e. ensuring that // we don't accidently overwrite newer files /* Queries the last-modification-time and size of the roaming files, from the OS and writes them all into a special local file. This file will also be uploaded and later be used to compare the ... */ saveFileStats : function() { }, // Pass back info to dialog/c++. /* returns int, really enum: 0 = do nothing, ignore all password/login changes 1 = Save password, set SavePW pref (user checked the box in the prompt) 2 = Set SavePassword pref to false and ignore Password/Username retval */ getSaveLogin : function() { if (this.saveLogin) return this.savePassword ? 1 : 2; else return 0; }, getPassword : function() { if (this.getSaveLogin() == 1) return this.password; else return null; }, getUsername : function() { if (this.getSaveLogin() == 1) return this.username; else return null; }, // helpful functions for callers // returns an array of TransferFiles with all files with a certain status filesWithStatus : function(status) { var result = new Array(); for (var i = 0, l = this.files.length; i < l; i++) if (this.files[i].status == status) result.push(this.files[i]); return result; }, // utility/debug /* Prints to the console the contents of this object. */ dump : function() { return; //ddump("Transfer object:"); //ddump(" download: " + this.download); // bool, see constructor //ddump(" serial: " + this.serial); // bool, see constructor //ddump(" localDir: " + this.localDir); // String, see constructor //ddump(" remoteDir: " + this.remoteDir); // String, see constructor //ddump(" password: " + this.password); // String, see constructor //ddump(" savePassword: " + this.savePassword); // bool, see constructor //ddump(" sitename: " + this.sitename); // String, to be displayed to the user identifying the remote server //ddump(" progress: "+(this.progressCalculate ? this.progress : "disabled")); // float, 0..1, how much of the files (measured by filesizes) has // already been transfered //ddump(" progressSizeAll: " + this.progressSizeAll); // int, in bytes, cumulated filesizes as reported by the owner //ddump(" Files: (" + this.files.length + " files)"); for (var i = 0, l = this.files.length; i < l; i++) { //ddump(" File " + i + ":"); this.files[i].dump(); } //ddump(" finished callbacks: " + this.finishedCallbacks.length); //ddump(" progress callbacks: " + this.progressCallbacks.length); //for (var i = 0, l = this.finishedCallbacks.length; i < l; i++) //dump(this.finishedCallbacks[i]); } } /* @param transfer Transfer the owner @param filenr int index of this file in the transfer's files array @param filename, mimetype, size see Transfer */ function TransferFile(transfer, filenr, // hooks to owner filename, mimetype, size) { // sanitize input if (mimetype == undefined || mimetype == "") mimetype = "application/octet-stream"; if (size == undefined) size = 0; // for explanations, see this.dump() this.filename = filename; this.mimetype = mimetype; this.size = size; //ddump("got file " + filename + " with mimetype " + mimetype); this.status = "pending"; this.statusCode = 0; this.progress = 0.0; this.channel = null; this.progressListener = null; this.fos = null; this.bos = null; this.localFile = null; this.transfer = transfer; this.filenr = filenr; } TransferFile.prototype = { dump : function() { //ddump(" filename: " + this.filename); // String: relative pathname from profile root //ddump(" mimetype: " + this.mimetype); // String. might be undefined. //ddump(" size: " + this.size); // int, in bytes. might be 0 for undefined. //ddump(" status: " + this.status); // String: "pending" (not started), "busy", "done", "failed" //ddumpCont(" statusCode: " + this.statusCode + " ("); //ddump(NameForStatusCode(this.statusCode) + ")"); // int: nsresult error code that we got from Necko //ddump(" httpResponse: " + this.httpResponse); // int: HTTP response code that we got from the server //ddump(" statusText: " + this.statusText); // String: Text corresponding to error (usually with httpResponse // or fatal errors). Might be in English or the server's language. // Text (usually translated) corresponding to statusCode can be // gotten from ...utility.js; it may include this text. //ddump(" progress: " + this.progress); // float: 0 (nothing) .. 1 (complete) }, /* Use this function to change this.status @param aStatus String like this.status, i.e. "done", "pending" etc. @param aStatusCode Mozilla error code @param aMessage String Sets statusText, a long explanation specific to this error case. Optional. */ setStatus : function(aStatus, aStatusCode, aMessage) { //ddumpCont("request to change "+this.filename+" from "+this.status+" ("); //ddumpCont(NameForStatusCode(this.statusCode)+") to "+aStatus+" ("); var undef = aStatusCode == undefined; //ddump((undef ? "no new status code" : NameForStatusCode(aStatusCode))+")"); if (this.statusCode == aStatusCode && this.status == aStatus) { //ddump(" (no changes)"); return; } var was_failed = (this.status == "failed"); var was_done = (this.status == "done"); if (!was_failed) /* prevent overwriting older errors with newer, bogus OK status codes or the real problem with consequential problems. */ { this.status = aStatus; if (aStatusCode != undefined) { this.statusCode = aStatusCode; //this.statusText = aMessage; // might be undefined, but it must match // // this.statusCode // WORKAROUND *sigh*, for FTP, we get the error msg before the error } if (aMessage) this.statusText = aMessage; for (var i = 0, l = this.transfer.progressCallbacks.length; i < l; i++) this.transfer.progressCallbacks[i](this.filenr); } if ( !(was_done || was_failed) && (aStatus == "done" || aStatus == "failed") ) // file just completed or failed this.transfer.fileFinished(this.filenr); } } /* Use one object per file to be downloaded. @param filenr integer index of file in transfer.files @param transfer Transfer the context */ function TransferProgressListener(transfer, filenr) { this.transfer = transfer; // this creates a cyclic reference :-( this.filenr = filenr; this.file = this.transfer.files[this.filenr]; var consumer = Components.classes["@mozilla.org/binaryoutputstream;1"] .createInstance(Components.interfaces .nsIBinaryOutputStream); /* I don't care about that stream, and it's empty anyways, so just use any outputstream that stores in RAM. nsStorageStream would probably have been more suited, but I don't see how I could instantiate that from JS, and binaryoutputstream works just as well. */ var buffer = Components .classes["@mozilla.org/network/buffered-output-stream;1"] .createInstance(Components.interfaces .nsIBufferedOutputStream); this.dummy_ssl = Components .classes["@mozilla.org/network/simple-stream-listener;1"] .createInstance(Components.interfaces .nsISimpleStreamListener); buffer.init(consumer, 32768); this.dummy_ssl.init(buffer, null); } TransferProgressListener.prototype = { // Progress // nsIRequestObserver onStartRequest : function(aRequest, aContext) { //ddump("onStartRequest: " + aRequest.name); this.file.setStatus("busy"); }, onStopRequest : function(aRequest, aContext, aStatusCode) { //ddump("onStopRequest:"); //ddump(" Request: " + aRequest.name); //ddump(" StatusCode: " + NameForStatusCode(aStatusCode)); if (aStatusCode == kNS_OK) { /* WORKAROUND HTTP gives us NS_OK, although the request failed with an HTTP error code, so check for that. FTP gives us NS_OK, although the transfer is still ongoing. I tried to work around that in onStatus(), see comment there. */ var scheme = this.file.channel.URI.scheme; if (scheme == "http") this.privateHTTPResponse(); //else if (scheme == "ftp") // return; else // let's hope that the other protocol impl.s are saner this.file.setStatus("done", aStatusCode); } else this.privateStatusChange(aStatusCode); }, // nsIProgressEventSink onStatus : function(aRequest, aContext, aStatusCode, aStatusArg) { /* WORKAROUND The status codes that are passed to us here in aStatusCode look like nsresult error codes, but their numerical values overlap with other, real errors. Darin said that real errors are never passed in here, so we don't have a hard conflict. To make processing easier, I just translate these status codes into made-up, unique error codes, so we can later check them together with other status and error codes and store them in the normal this.status field (which contains normal XPCOM error codes) etc.. */ if (aStatusCode == kStatusResolvingHost_Status) aStatusCode = kStatusResolvingHost; else if (aStatusCode == kStatusConnectedTo_Status) aStatusCode = kStatusConnectedTo; else if (aStatusCode == kStatusSendingTo_Status) aStatusCode = kStatusSendingTo; else if (aStatusCode == kStatusReceivingFrom_Status) aStatusCode = kStatusReceivingFrom; else if (aStatusCode == kStatusConnectingTo_Status) aStatusCode = kStatusConnectingTo; else if (aStatusCode == kStatusWaitingFor_Status) aStatusCode = kStatusWaitingFor; else if (aStatusCode == kStatusReadFrom_Status) aStatusCode = kStatusReadFrom; else if (aStatusCode == kStatusWroteTo_Status) aStatusCode = kStatusWroteTo; //ddump("onStatus:"); //ddump(" Request: " + aRequest.name); //ddump(" StatusCode: " + NameForStatusCode(aStatusCode)); //ddump(" StatusArg: " + aStatusArg); /* WORKAROUND We sometimes get onStopRequest *before* e.g. onStatus(SENDING_TO), so ignore all status msgs after onstop. This is dangerous, because IIRC I saw onStop also on the very beginning of a transfer, and if that happens for the last file, we might close the dialog before the transfer finished, but I don't know another quick and not too bad workaround. */ if (this.file.status == "done") return; this.privateStatusChange(aStatusCode); }, /* Use this function to interpret Mozilla status/error codes and update |this| appropriately. Don't use it for NS_OK - what this means depends on the situation and thus can't be interpreted here. @param aStatusCode Mozilla error code */ privateStatusChange : function(aStatusCode) { var status; if (aStatusCode == kNS_OK) // don't know what this means, only the caller knows what succeeded return; else if (aStatusCode == kStatusReadFrom) status = "busy"; else if (aStatusCode == kStatusReceivingFrom) status = "busy"; else if (aStatusCode == kStatusSendingTo) status = "busy"; else if (aStatusCode == kStatusWaitingFor) status = "busy"; else if (aStatusCode == kStatusResolvingHost) status = "busy"; else if (aStatusCode == kStatusConnectedTo) status = "busy"; else if (aStatusCode == kStatusConnectingTo) status = "busy"; else if (aStatusCode == kErrorBindingRedirected) status = "busy"; else if (aStatusCode == kErrorBindingRetargeted) status = "busy"; else if (aStatusCode == kStatusBeginFTPTransaction) status = "busy"; else if (aStatusCode == kStatusEndFTPTransaction) status = "done"; else if (aStatusCode == kInProgress) status = "busy"; else if (aStatusCode == kStatusFTPLogin) status = "busy"; else if (aStatusCode == kErrorFTPAuthNeeded) status = "busy"; else if (aStatusCode == kNetReset) ; // correct? else status = "failed"; this.file.setStatus(status, aStatusCode); }, /* Use this function to get and interpret HTTP status/error codes and update |this| appropriately. */ privateHTTPResponse : function() { //ddumpCont("privateHTTPResponse "); // Get HTTP response code var responseCode; var respText; try { var httpchannel = this.file.channel .QueryInterface(Components.interfaces.nsIHttpChannel); responseCode = httpchannel.responseStatus; this.file.httpResponse = responseCode; respText = httpchannel.responseStatusText; //ddump(responseCode); } catch(e) { //ddump(" Error while trying to get HTTP response: " + e); this.file.setStatus("failed", kErrorNoInterface); return; } // Interpret HTTP response code var status; if (responseCode >= 100 && responseCode < 200) status = "busy"; else if (responseCode >= 200 && responseCode < 300) status = "done"; else if (responseCode >= 300 && responseCode < 400) status = "busy"; else if (responseCode >= 400 && responseCode < 600) status = "failed"; else // what? return; this.file.setStatus(status, kStatusHTTP, respText); }, onProgress : function(aRequest, aContext, aProgress, aProgressMax) { //ddump("onProgress: " + aProgress + "/" + aProgressMax); if (aProgressMax > 0 && aProgress > 0) // WORKAROUND Necko sometimes sends crap like 397/0 or 0/4294967295 { this.transfer.progressChanged(this.filenr, aProgress / aProgressMax); } }, // Dummies // nsStreamListener onDataAvailable : function(aRequest, aContext, anInputStream, anOffset, aCount) { /* dummy, because it's only the down stream during an upload and during download, we use the tee, which copies to the outstream. */ this.dummy_ssl.onDataAvailable(aRequest, aContext, anInputStream, anOffset, aCount); }, // nsIFTPEventSink, nsIHTTPEventSink OnFTPControlLog : function(aServer, aMsg) { // dummy }, onChannelRedirect : function(aOldChannel, aNewChannel, aFlags) { // dummy }, /* // nsIDocShellTreeItem (wtf?) name : "dummy in transfer.js", itemType : 0, parent : null, sameTypeParent : null, rootTreeItem : null, sameTypeRootTreeItem : null, treeOwner : null, childOffset : 0, nameEquals : function(name) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, findItemWithName : function(name, requestor) { throw Components.results.NS_ERROR_NOT_IMPLEMENTED; }, */ // Prompts // nsIPrompt alert : function(dlgTitle, text) { //ddump("alert " + text); /* WORKAROUND FTP sends us these in the case of an error *sigh*. Don't display dialog, but redirect to file errors. */ this.file.statusText = text.replace(/\n/, ""); /* if (!dlgTitle) title = GetString("Alert"); GetPromptService().alert(window, dlgTitle, text); */ }, alertCheck : function(dlgTitle, text, checkBoxLabel, checkObj) { //ddump("alertCheck"); this.alert(dlgTitle, text); }, confirm : function(dlgTitle, text) { //ddump("confirm"); return this.confirmCheck(dlgTitle, text, null, null); }, confirmCheck : function(dlgTitle, text, checkBoxLabel, checkObj) { //ddump("confirmCheck"); var promptServ = GetPromptService(); if (!promptServ) return; promptServ.confirmEx(window, dlgTitle, text, nsIPromptService.STD_OK_CANCEL_BUTTONS, "", "", "", checkBoxLabel, checkObj); }, confirmEx : function(dlgTitle, text, btnFlags, btn0Title, btn1Title, btn2Title, checkBoxLabel, checkVal) { //ddump("confirmEx"); var promptServ = GetPromptService(); if (!promptServ) return 0; return promptServ.confirmEx(window, dlgTitle, text, btnFlags, btn0Title, btn1Title, btn2Title, checkBoxLabel, checkVal); }, select : function(dlgTitle, text, count, selectList, outSelection) { //ddump("select"); var promptServ = GetPromptService(); if (!promptServ) return false; return promptServ.select(window, dlgTitle, text, count, selectList, outSelection); }, prompt : function(dlgTitle, label, /*inout*/ inputvalueObj, checkBoxLabel, /*inout*/ checkObj) { //ddump("nsIPrompt::prompt"); return this.privatePrompt(dlgTitle, label, null, inputvalueObj, checkBoxLabel, checkObj); }, /* Warning: these |promptPassword| and |promptUsernameAndPassword| from nsIPrompt will never be invoked. If they are being called, the nsIAuthPrompt versions will be invoked, with wrong arguments. This is because we're overloading the functions with the same number of parameters and JS can't distinguish between different parameter types. This is a bug copied from Editor's Publishing feature; the bug exists there as well. */ promptPassword : function(dlgTitle, label, /*inout*/ pwObj, checkBoxLabel, /*inout*/ savePWObj) { //ddump("nsIPrompt::promptPassword:"); //ddump(" pwObj: " + pwObj.value); //ddump(" checkBoxLabel: " + checkBoxLabel); //ddump(" savePWObj: " + savePWObj.value + " (ignored)"); var ret = this.privatePromptPassword(dlgTitle, label, null, pwObj, checkBoxLabel); savePWObj.value = this.transfer.savePassword; // out param return ret; }, promptUsernameAndPassword : function(dlgTitle, label, /*inout*/ userObj, /*inout*/ pwObj, savePWLabel, /*inout*/ savePWObj) { //ddump("nsIPrompt::promptUsernameAndPassword:"); //ddump(" userObj: " + userObj.value); //ddump(" pwObj: " + pwObj.value); //ddump(" savePWLabel: " + savePWLabel); //ddump(" savePWObj: " + savePWObj.value + " (ignored)"); var ret = this.privatePromptUsernameAndPassword(dlgTitle, label, null, userObj, pwObj, savePWLabel); savePWObj.value = this.transfer.savePassword; // out param return ret; }, // nsIAuthPrompt prompt : function(dlgTitle, label, pwrealm, /*in*/ savePW, defaultText, /*out*/ resultObj) { //ddump("nsIAuthPrompt::prompt"); var savePWObj = {value:this.transfer.savePassword}; // I know it better than the caller. var inputvalueObj = {value:defaultText}; var ret = this.privatePrompt(dlgTitle, label, pwrealm, inputvalueObj, null, savePWObj); resultObj.value = inputvalueObj.value; return ret; }, promptPassword : function(dlgTitle, label, pwrealm, /*in*/ savePW, /*out*/ pwObj) { //ddump("nsIAuthPrompt::promptPassword"); //ddump(" pwrealm: " + pwrealm); //ddump(" savePW: " + savePW + " (ignored)"); //ddump(" pwObj: " + pwObj.value); return this.privatePromptPassword(dlgTitle, label, pwrealm, pwObj, null); }, promptUsernameAndPassword : function(dlgTitle, label, pwrealm, /*in*/savePW, /*out*/ userObj, /*out*/ pwObj) { //ddump("nsIAuthPrompt::promptUsernameAndPassword:"); //ddump(" pwrealm: " + pwrealm); //ddump(" savePW: " + savePW + " (ignored)"); //ddump(" userObj: " + userObj.value); //ddump(" pwObj: " + pwObj.value); return this.privatePromptUsernameAndPassword(dlgTitle, label, pwrealm, userObj, pwObj, null); }, // helper functions for prompt stuff privatePrompt : function(dlgTitle, label, pwrealm, /*inout*/ inputvalueObj, checkBoxLabel, /*inout*/ checkObj) { if (!dlgTitle) dlgTitle = GetString("PasswordTitle"); if (!pwrealm) pwrealm = this.transfer.sitename; if (!label) label = this.makePWLabel(userObj.value, pwrealm); if (!checkBoxLabel) checkBoxLabel = GetString("SavePassword"); var promptServ = GetPromptService(); if (!promptServ) return false; var ret = promptServ.prompt(window, dlgTitle, label, inputvalueObj, checkBoxLabel, checkObj); if (!ret) this.transfer.cancel(); return ret; }, privatePromptPassword : function(dlgTitle, label, pwrealm, /*inout*/ pwObj, savePWLabel) { //ddump("privatePromptPassword()"); //ddump(" pwObj.value: " + pwObj.value); //ddump(" savePWLabel: " + savePWLabel); //ddump(" label: " + label); //ddump(" pwrealm: " + pwrealm); //ddump(" this.transfer.savePassword: " + this.transfer.savePassword); //ddump(" this.transfer.username: " + this.transfer.username); if (!dlgTitle) dlgTitle = GetString("PasswordTitle"); if (!pwrealm) pwrealm = this.transfer.sitename; if (!label) label = this.makePWLabel(userObj.value, pwrealm); if (!savePWLabel) savePWLabel = GetString("SavePassword"); var savePWObj = {value: this.transfer.savePassword}; //ddump(" pwObj.value: " + pwObj.value); //ddump(" savePWLabel: " + savePWLabel); //ddump(" label: " + label); //ddump(" pwrealm: " + pwrealm); var promptServ = GetPromptService(); if (!promptServ) return false; var ret = false; try { ret = promptServ.promptPassword(window, dlgTitle, label, pwObj, savePWLabel, savePWObj); if (!ret) this.transfer.cancel(); else this.updateUsernamePasswordFromPrompt(this.transfer.username, pwObj.value, savePWObj.value); } catch (e) { dumpError(e); } return ret; }, privatePromptUsernameAndPassword : function(dlgTitle, label, pwrealm, /*inout*/ userObj, /*inout*/ pwObj, savePWLabel) { // HTTP prompts us twice even if user Cancels from 1st attempt! //ddump("privatePromptUsernameAndPassword()"); //ddump(" pwObj.value: " + pwObj.value); //ddump(" userObj.value: " + userObj.value); //ddump(" savePWLabel: " + savePWLabel); //ddump(" label: " + label); //ddump(" pwrealm: " + pwrealm); //ddump(" this.transfer.savePassword: " + this.transfer.savePassword); //ddump(" this.transfer.username: " + this.transfer.username); if (!dlgTitle) dlgTitle = GetString("PasswordTitle"); if (!pwrealm) pwrealm = this.transfer.sitename; if (!label) label = this.makePWLabel(userObj.value, pwrealm); if (!savePWLabel) savePWLabel = GetString("SavePassword"); if (!userObj.value) // HTTP PUT uses this dialog if either username or password is bad, // so prefill username with the previous value userObj.value = this.transfer.username; var savePWObj = {value: this.transfer.savePassword}; //ddump(" savePWObj.value: " + savePWObj.value); //ddump(" userObj.value: " + userObj.value); //ddump(" savePWLabel: " + savePWLabel); //ddump(" label: " + label); //ddump(" pwrealm: " + pwrealm); /* I had the idea to put up the password-only dialog, if we have a username. But if the user didn't enter a username in the prefs, it first all works correctly, the username+password dialog comes up. The problem starts when the user entered the wrong username - he won't have a chance to change it anymore. Not sure, if that is bad. So, the code for that is below. Update: I decided that the user has to enter a username in any case, and he can't change it during login. At least not in this case. */ if (userObj.value && userObj.value != "") { // |label| here says "enter username and password ...", so we need // to build our own. label = this.makePWLabel(userObj.value, pwrealm); return this.privatePromptPassword(dlgTitle, label, pwrealm, pwObj, null); } // We should get here only, if we got an empty username pref from the reg. //ddump(" asking for username as well"); var ret = false; try { var promptServ = GetPromptService(); if (!promptServ) return false; ret = promptServ.promptUsernameAndPassword(window, dlgTitle, label, userObj, pwObj, savePWLabel, savePWObj); if (!ret) this.transfer.cancel(); else this.updateUsernamePasswordFromPrompt(userObj.value, pwObj.value, savePWObj.value); } catch (e) { dumpError(e); } //ddump(" done. result:"); //ddump(" userObj.value: " + userObj.value); //ddump(" pwObj.value: " + pwObj.value); //ddump(" savePWObj.value: " + savePWObj.value); return ret; }, // Update any data that the user supplied in a prompt dialog updateUsernamePasswordFromPrompt : function(username, password,savePassword) { // Set flag to save login data after transferring, if it changed in // dialog and the "SavePassword" checkbox was checked this.transfer.saveLogin = (username != this.transfer.username || password != this.transfer.password) && savePassword; //ddump(" username: " + username); //ddump(" password: " + password); //ddump(" savePW: " + savePassword); //ddump(" saveLogin: " + this.transfer.saveLogin); this.transfer.username = username; this.transfer.password = password; this.transfer.savePassword = savePassword; var uri = GetIOService().newURI(this.transfer.remoteDir, null, null); uri.username = username; this.transfer.remoteDir = uri.spec; // doesn't work, the spec isn't updated this.transfer.dump(); }, makePWLabel : function (user, realm) { if (!user) user = ""; var label = GetString("EnterPasswordForUserAt"); label = label.replace(/%username%/, user); label = label.replace(/%realm%/, realm); return label; }, // XPCOM // nsISupports QueryInterface : function(aIID) { // ddump("QI: " + aIID.toString() + "\n"); if (aIID.equals(Components.interfaces.nsIProgressEventSink) || aIID.equals(Components.interfaces.nsIRequestObserver) || aIID.equals(Components.interfaces.nsIStreamListener) || aIID.equals(Components.interfaces.nsISupports) || aIID.equals(Components.interfaces.nsISupportsWeakReference) || aIID.equals(Components.interfaces.nsIPrompt) || aIID.equals(Components.interfaces.nsIAuthPrompt) || aIID.equals(Components.interfaces.nsIFTPEventSink) || aIID.equals(Components.interfaces.nsIChannelEventSink) || aIID.equals(Components.interfaces.nsIDocShellTreeItem) || aIID.equals(Components.interfaces.nsIInterfaceRequestor)) return this; throw Components.results.NS_NOINTERFACE; }, // nsIInterfaceRequestor getInterface : function(aIID) { return this.QueryInterface(aIID); } } var gIOService; function GetIOService() { if (gIOService) return gIOService; // throw errors into caller gIOService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); return gIOService; }