/*************************************************************************** filetransfer.cpp - description ------------------- begin : Mon Mar 24 2003 copyright : (C) 2003 by Mike K. Bennett (C) 2005 by Diederik van der Boor email : mkb137b@hotmail.com vdboor --at-- codingdomain.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "filetransfer.h" #include "../../utils/kmessconfig.h" #include "../../currentaccount.h" #include "../../kmessdebug.h" #include "../extra/msnftpconnection.h" #include "../mimemessage.h" #include <QFile> #include <QFileInfo> #include <KLocale> // It wouldn't hurt if these GUI specific features are removed here. // That would make this class GUI-independant and only emit signals for possible GUI actions. #include <KFileDialog> #include <KMessageBox> #include "../../dialogs/transferwindow.h" #ifdef KMESSDEBUG_FILETRANSFER #define KMESSDEBUG_FILETRANSFER_GENERAL #endif // The constructor without filename (sufficient for incoming sessions) FileTransfer::FileTransfer(const QString &contactHandle) : MimeApplication(contactHandle) , connectionEstablished_(false) , connectivity_('?') , file_(0) , msnFtpConnection_(0) , transferID_(-1) { setApplicationType( ChatMessage::TYPE_APPLICATION_FILE ); } // The constructor with filename to start a session FileTransfer::FileTransfer(const QString &contactHandle, const QString &filename) : MimeApplication(contactHandle) , connectionEstablished_(false) , connectivity_('?') , file_(0) , filePath_(filename) , msnFtpConnection_(0) , transferID_(-1) { setApplicationType( ChatMessage::TYPE_APPLICATION_FILE ); } // The destructor FileTransfer::~FileTransfer() { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug(); #endif // Displays the cancel state in the transfer window TransferWindow::getInstance()->failTransfer( transferID_ ); // Stop the ftp connection delete msnFtpConnection_; // Close the file nicely if(file_ != 0) { delete file_; } } // Connect signals of the MsnFtpConnection object void FileTransfer::connectMsnFtpConnection() { #ifdef KMESSTEST KMESS_ASSERT( msnFtpConnection_ != 0 ); #endif connect(msnFtpConnection_, SIGNAL( connectionEstablished() ) , // The direct connection was established this, SLOT( slotMsnFtpConnectionEstablished() ) ); connect(msnFtpConnection_, SIGNAL( statusMessage(QString,int) ) , // Display a message in the dialog/chat window this, SLOT( slotMsnFtpStatusMessage(QString,int) ) ); connect(msnFtpConnection_, SIGNAL( statusMessage(KLocalizedString,int) ) , // Display a message in the dialog/chat window this, SLOT( slotMsnFtpStatusMessage(KLocalizedString,int) ) ); connect(msnFtpConnection_, SIGNAL( transferComplete() ) , // Signal that the transfer is complete this, SLOT( slotMsnFtpTransferComplete() ) ); connect(msnFtpConnection_, SIGNAL( transferFailed() ) , // Signal that the transfer failed (and clean up) this, SLOT( slotMsnFtpTransferFailed() ) ); connect(msnFtpConnection_, SIGNAL( transferProgess(unsigned long) ) , // Signal that the transfer made progress this, SLOT( slotMsnFtpTransferProgess(unsigned long) ) ); } /** * The contact cancelled the session */ 00119 void FileTransfer::contactAborted(const QString &message) { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug(); #endif Q_UNUSED( message ); // Displays the cancel state in the transfer window TransferWindow::getInstance()->failTransfer( transferID_, i18n("Cancelled") ); // Also display it in chat modifyOfferMessage(); if( message.isEmpty() ) { showEventMessage( getContactAbortMessage(), ChatMessage::CONTENT_APP_CANCELED, true ); } else { showEventMessage( message, ChatMessage::CONTENT_APP_CANCELED, true ); } } // Step one of a contact-started chat: the contact invites the user 00145 void FileTransfer::contactStarted1_ContactInvitesUser(const MimeMessage& message) { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug(); #endif // Get the file name and size from the message suggestedFileName_ = message.getValue("Application-File"); fileSize_ = message.getValue("Application-FileSize").toInt(); connectivity_ = '?'; // Get the optional fields. if( message.hasField("Connectivity") ) { QString connectivity( message.getValue("Connectivity") ); if( connectivity.size() > 0 ) { connectivity_ = connectivity.at(0).toLatin1(); } } // Send the message to the chat window. offerAcceptOrReject( i18n( "The contact wants to send you a file: "%1" (%2).", "<span class=\"filename invitationFilename\">" + suggestedFileName_ + "</span>", toReadableBytes( fileSize_ ) ) ); } // Step two of a contact-started chat: the user accepts 00175 void FileTransfer::contactStarted2_UserAccepts() { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug() << "Starting."; #endif #ifdef KMESSTEST KMESS_ASSERT( file_ == 0 ); KMESS_ASSERT( filePath_.isEmpty() ); #endif QFileInfo fileInfo; // Open the options and get the default downloads directory KConfigGroup group = KMessConfig::instance()->getGlobalConfig( "General" ); QString startFolder( group.readEntry( "receivedFilesDir", QDir::homePath() ) ); if( group.readEntry( "useReceivedFilesDir", false ) && ! startFolder.isEmpty() ) { int index = -1; do { // The ++ ensures that at the next iteration a suffix is appended to the name if( index++ < 0 ) { filePath_ = startFolder + "/" + suggestedFileName_; } else { fileInfo.setFile( suggestedFileName_ ); filePath_ = startFolder + "/" + fileInfo.baseName() + "(" + QString::number( index ) + ")." + fileInfo.completeSuffix(); } } while( QFile::exists( filePath_ ) ); fileInfo.setFile( filePath_ ); fileName_ = fileInfo.fileName(); } else { // Ask the user to specify where to put the file QString recentFolderTag( ":filedownload" ); // Open a file dialog so that the user can save the file to a particular directory and name. bool hasFile = false; while( ! hasFile ) { // Set an initial path to the file KUrl startDir = KFileDialog::getStartUrl( startFolder, recentFolderTag ); startDir.addPath( suggestedFileName_ ); delayDeletion( true ); // Ask the user for a file. filePath_ = KFileDialog::getSaveFileName( startDir.url() ); delayDeletion( false ); if( isClosing() ) { endApplication(); return; } if( filePath_.isNull() ) { #ifdef KMESSDEBUG_FILETRANSFER_P2P kDebug() << "User cancelled in file save dialog"; #endif // Dialog cancelled, cancel afterall modifyOfferMessage(); userRejected(); return; } hasFile = true; fileInfo.setFile( filePath_ ); fileName_ = fileInfo.fileName(); // Check if the selected file exists and if the user wants to overwrite it. // The while loop is for the prompt to keep appearing if the user // chooses the same filename but does not want to overwrite the file. if( QFile::exists(filePath_) ) { if( KMessageBox::warningContinueCancel( 0, i18n("The file "%1" already exists.\ndo you want to overwrite it?", fileName_ ), i18n("Overwrite File"), KGuiItem( i18n("Over&write") ) ) == KMessageBox::Cancel ) { // User does not want to overwrite hasFile = false; startFolder = fileInfo.path(); } } } } // Now we try to open the file file_ = new QFile(filePath_); bool success = (file_ != 0) && file_->open(QIODevice::WriteOnly); if( ! success ) { #ifdef KMESSDEBUG_FILETRANSFER_P2P kDebug() << "Cancelling session"; #endif // Notify the user, even if debug mode is not enabled. kWarning() << "Unable to open file: " << filePath_ << "!"; // Close the file (also causes gotData() to fail) delete file_; file_ = 0; // Tell the user about it modifyOfferMessage(); showEventMessage( i18n("The transfer of file "%1" failed. Couldn't save the file.", "<span class=\"filename failedFilename\">" + fileName_ + "</span>"), ChatMessage::CONTENT_APP_FAILED, false ); slotMsnFtpStatusMessage( i18n( "The transfer of %1 failed. Couldn't open the destination file." ), MsnFtpConnection::STATUS_CHAT ); // Send 500 Internal Error back if we failed (this is still the accept stage) sendCancelMessage( CANCEL_FAILED ); return; } #ifdef KMESSDEBUG_FILETRANSFER_P2P kDebug() << "Sending accept message"; #endif // All passed. Send the accept message MimeMessage message; message.addField( "Invitation-Command", "ACCEPT" ); message.addField( "Invitation-Cookie", getCookie() ); message.addField( "Launch-Application", "FALSE" ); message.addField( "Request-Data", "IP-Address:" ); sendMessage( message ); // Notify the user modifyOfferMessage(); showEventMessage( i18n("Transfer accepted."), ChatMessage::CONTENT_APP_STARTED, false ); } // Step three of a contact-started chat: the contact confirms the accept 00325 void FileTransfer::contactStarted3_ContactConfirmsAccept(const MimeMessage& message) { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug(); #endif #ifdef KMESSTEST KMESS_ASSERT( ! fileName_.isEmpty() ); KMESS_ASSERT( ! suggestedFileName_.isEmpty() ); KMESS_ASSERT( file_ != 0 ); KMESS_ASSERT( msnFtpConnection_ == 0 ); #endif QString ip; QString ipInternal; quint16 port; int portXInternal; int portX; QString authCookie; // Pull the IP, port, and authorization info from the message ip = message.getValue( "IP-Address" ); ipInternal = message.getValue( "IP-Address-Internal" ); // As of MSN5 portXInternal = message.getValue( "PortX-Internal" ).toInt(); // As of MSN5 port = (quint16)message.getValue( "Port" ).toUInt(); portX = message.getValue( "PortX" ).toInt(); // As of MSN5 authCookie = message.getValue( "AuthCookie" ); /* * This is an attempt to get file transfers * between two home systems working.. (both behind the same NAT router) * I didn't discover any reference when I started this. * * TODO: Implement MSN5-compatible transfers using http://www.hypothetic.org/docs/msn/phorum/read.php?f=1&i=3435&t=3372#reply_3435 * A full implementation also requires UPnP handling. * * When receiving an invite with a "Connectivity: N" field, * and the client it's not being translated by a NAT, set a "Sender-Connect: TRUE" field, * and include addressing information for both internal and external addresses, and start a server. * Otherwise, start a client. * * Presumably, a client can try three connections when attempting to connect to a server: * - To IP-Address on Port * - To IP-Address on PortX * - To IP-Address-Internal on PortX-Internal. * Using IP-Address on Port and IP-Address-Internal on PortX-Internal should be sufficient. */ QString externalIp( getExternalIp() ); if(ip == externalIp) { // The other client must be at the same network..! // Try to connect internally. // Just to be sure the values are set... if(! ipInternal.isEmpty()) { ip = ipInternal; //port = internPort; port = 6891; // Only works if there is one transfer, but it's better then nothing. } // else: the file transfer fails anyway } #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug() << "IP: " << ip << " Port: " << port << " AuthCookie: " << authCookie; #endif // Initialize the MSNFTP connection msnFtpConnection_ = new MsnFtpConnection( CurrentAccount::instance()->getHandle(), authCookie ); connectMsnFtpConnection(); // Initialize the progess dialog initializeProgressDialog(true); slotMsnFtpStatusMessage( i18n( "Connecting to %1, port %2", ip, QString::number( port ) ), MsnFtpConnection::STATUS_DIALOG ); // Start the transfer msnFtpConnection_->retrieveFile(file_, ip, port); } // Return the application's GUID QString FileTransfer::getAppId() { return "{5D3E02AB-6190-11d3-BBBB-00C04F795683}"; } /** * Return a cancel message to display. */ 00418 QString FileTransfer::getContactAbortMessage() const { // Application::getUserAbortMessage() returns "The contact cancelled the session". return i18n("The contact has cancelled the transfer of file "%1".", ( fileName_.isEmpty() ? suggestedFileName_ : fileName_ ) ); } /** * Return a cancel message to display. */ 00429 QString FileTransfer::getUserAbortMessage() const { // Application::getUserAbortMessage() returns "You have cancelled the session". return i18n( "You have cancelled the transfer of file "%1".", ( fileName_.isEmpty() ? suggestedFileName_ : fileName_ ) ); } /** * Return a cancel message to display. */ 00440 QString FileTransfer::getUserRejectMessage() const { // Application::getUserAbortMessage() returns "You have cancelled the session". return i18n( "You have rejected the transfer of file "%1".", ( fileName_.isEmpty() ? suggestedFileName_ : fileName_ ) ); } // Create and initilize the progress dialog. void FileTransfer::initializeProgressDialog(bool incoming) { #ifdef KMESSTEST KMESS_ASSERT( fileSize_ != 0 ); KMESS_ASSERT( fileName_.length() > 0 ); #endif // Create a new entry in the tranfer window TransferWindow *transferWindow = TransferWindow::getInstance(); transferID_ = transferWindow->addEntry( fileName_, fileSize_, incoming ); // Connect the dialog so that if the user closes it, it's deleted. connect( transferWindow, SIGNAL( cancelTransfer(int) ) , this, SLOT( slotCancelTransfer(int) ) ); } // Cancelled the file transfer from the TransferWindow void FileTransfer::slotCancelTransfer( int transferID ) { // Check if the cancelled transfer is ours if( transferID_ != transferID ) { return; } #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug(); #endif // Call userAborted() which cleans up this object correctly userAborted(); } // The direct connection was established void FileTransfer::slotMsnFtpConnectionEstablished() { // Update the status dialog slotMsnFtpStatusMessage( i18n("Connection established"), MsnFtpConnection::STATUS_DIALOG ); connectionEstablished_ = true; } // Display a status message (as a raw localization string) void FileTransfer::slotMsnFtpStatusMessage( KLocalizedString message, int statusType ) { // Parse the %1 placeholder for file name switch( statusType ) { case MsnFtpConnection::STATUS_DIALOG: case MsnFtpConnection::STATUS_DIALOG_FAILED: message = message.subs( fileName_ ); break; // A message to display in the chat window case MsnFtpConnection::STATUS_CHAT_FAILED: message = message.subs( "<span class=\"filename failedFilename\">" + fileName_ + "</span>" ); break; case MsnFtpConnection::STATUS_CHAT: default: message = message.subs( "<span class=\"filename\">" + fileName_ + "</span>" ); break; } // Call the real handler with the completed localization string slotMsnFtpStatusMessage( message.toString(), statusType ); } // Display a status message void FileTransfer::slotMsnFtpStatusMessage(QString message, int statusType) { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug() << "Status message (type=" << statusType << " message=" << message << ")."; #endif switch(statusType) { // A message for the transfer window case MsnFtpConnection::STATUS_DIALOG: { TransferWindow::getInstance()->setStatusMessage( transferID_, message ); break; } case MsnFtpConnection::STATUS_DIALOG_FAILED: { TransferWindow::getInstance()->failTransfer( transferID_, message ); break; } // A message to display in the chat window case MsnFtpConnection::STATUS_CHAT_FAILED: { showEventMessage( message, ChatMessage::CONTENT_APP_FAILED, ! isUserStartedApp() ); break; } case MsnFtpConnection::STATUS_CHAT: default: { showEventMessage( message, ChatMessage::CONTENT_APP_FAILED, ! isUserStartedApp() ); break; } } } // Signal that the transfer was succesful void FileTransfer::slotMsnFtpTransferComplete() { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug() << "Transfer successful."; #endif // Displays the success in the transfer window TransferWindow::getInstance()->finishTransfer( transferID_ ); // Send an event to the chat QString fileURL( "<span class=\"filename completedFilename\">" "<a href=\"file:" + filePath_ + "\">" + fileName_ + "</a>" "</span>" ); // Show an appropriate message QString message; if( isUserStartedApp() ) { message = i18n( "Successfully sent file "%1".", fileURL ); } else { message = i18n( "Successfully received file "%1".", fileURL ); } showEventMessage( message, ChatMessage::CONTENT_APP_ENDED, ! isUserStartedApp() ); } // Signal that the transfer failed void FileTransfer::slotMsnFtpTransferFailed() { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug() << "Transfer failed."; #endif // If MsnFtpConnection did not emit a statusMessage(.. STATUS_DIALOG_FAILED) signal // the dialog will be terminated with a default message here. TransferWindow::getInstance()->failTransfer( transferID_ ); // If the connection attempt failed, send a cancel message. // In other situations, the remote client will notice the MSNFTP connection closed. if( ! connectionEstablished_ ) { sendCancelMessage(CANCEL_FAILED); // calls endApplication() already return; } // Terminate this application endApplication(); } // Update the progress dialog with the number of bytes transferred. void FileTransfer::slotMsnFtpTransferProgess(unsigned long bytesReceived) { // Display the progress update in the transfer window TransferWindow::getInstance()->updateProgress( transferID_, bytesReceived ); } // Convert a string to some more readable form QString FileTransfer::toReadableBytes( ulong bytes ) { QString format; if(bytes > 1048576) { // Using '%.2f' instead of '%.1f' removes the ".0" part, but it's less pretty. format.sprintf("%.1f", (double) bytes / 1048576.0); return i18n( "%1 MB", format ); } else if(bytes > 1024) { format.sprintf("%.1f", (double) bytes / 1024.0); return i18n( "%1 kB", format ); } else { return i18n( "%1 bytes", bytes ); } } // The user cancelled the session 00651 void FileTransfer::userAborted() { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug(); #endif // Display the cancel state in the transfer window TransferWindow::getInstance()->failTransfer( transferID_, i18n("Cancelled") ); // If there is no MSNFTP connection, the standard behavour is exactly what we need. if(msnFtpConnection_ == 0) { MimeApplication::userAborted(); return; } // We have a MsnFtpConnection listening // Then teardown the active transfer if(msnFtpConnection_->isConnected()) { // It is already sending the file // Initiate connection teardown // Disconnect signals // We are not interested in status messages, they are displayed here already // slotWriteData() calls slotMsnFtpTransferFailed() which deletes this object, we do this here already disconnect(msnFtpConnection_, SIGNAL(statusMessage(QString,int)), this, SLOT(slotMsnFtpStatusMessage(QString,int))); disconnect(msnFtpConnection_, SIGNAL(transferFailed()), this, SLOT(slotMsnFtpTransferFailed())); // Tell MsnFtpConnection to stop transferring. This also signals the other client we cancelled // delete after all signals are processed (since we detached slotMsnFtpTransferFailed()) msnFtpConnection_->cancelTransfer(true); msnFtpConnection_->deleteLater(); msnFtpConnection_ = 0; // we don't have to send a cancel message anymore. // MsnFtpConnection already does the same thing. // Finally delete this object endApplication(); return; } else { // No transfer, but close the connection ASAP msnFtpConnection_->closeConnection(); msnFtpConnection_->deleteLater(); msnFtpConnection_ = 0; // The transfer was not initiated. // Send the cancel-session message. MimeApplication::userAborted(); return; } } // Step one of a user-started chat: the user invites the contact 00711 void FileTransfer::userStarted1_UserInvitesContact() { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug(); #endif #ifdef KMESSTEST KMESS_ASSERT( ! filePath_.isEmpty() ); #endif QString sizeString; MimeMessage message; file_ = new QFile(filePath_); bool success = (file_ != 0) && file_->open(QIODevice::ReadOnly); // Stop if the file can't be openend. if( ! success ) { #ifdef KMESSDEBUG_FILETRANSFER_P2P kDebug() << "Cancelling session"; #endif // Notify the user, even if debug mode is not enabled. kWarning() << "Unable to open file: " << filePath_ << "!"; // Close the file delete file_; file_ = 0; // Tell the user about it if( ! QFile::exists(filePath_) ) { showEventMessage( i18n( "The transfer of file "%1" failed. The file does not exist.", "<span class=\"filename failedFilename\">" + filePath_ + "</span>"), ChatMessage::CONTENT_APP_CANCELED, false ); } else { showEventMessage( i18n( "The transfer of file "%1" failed. The file could not be read.", "<span class=\"filename failedFilename\">" + filePath_ + "</span>"), ChatMessage::CONTENT_APP_CANCELED, false ); } return; } // Set the short file name QFileInfo fileInfo( filePath_ ); fileName_ = fileInfo.fileName(); // Read the file parameters fileSize_ = (uint)file_->size(); sizeString = QString::number(fileSize_); // Create the invitation message // connectivity_ = 'U'; // Or 'N' if the port is mapped with NAT message.addField( "Application-Name", "File Transfer" ); message.addField( "Application-GUID", getAppId() ); message.addField( "Invitation-Command", "INVITE" ); message.addField( "Invitation-Cookie", getCookie() ); message.addField( "Application-File", fileName_ ); message.addField( "Application-FileSize", sizeString ); // message.addField( "Connectivity", connectivity_ ); sendMessage( message ); // Give the user the option of cancelling the transfer offerCancel( i18n( "Sending file "%1" (%2).", "<span class=\"filename invitationFilename\">" + fileName_ + "</span>", toReadableBytes( fileSize_ ) ) ); } // Step two of a user-started chat: the contact accepts 00782 void FileTransfer::userStarted2_ContactAccepts(const MimeMessage& /*message*/) { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug(); #endif #ifdef KMESSTEST KMESS_ASSERT( msnFtpConnection_ == 0 ); #endif QString html; QString portString; QString authCookie; // Remove the accept links modifyOfferMessage(); // Create a message showing that the transfer was accepted. showEventMessage( i18n( "The contact has accepted the transfer of file "%1".", fileName_ ), ChatMessage::CONTENT_APP_STARTED, true ); // Get the authorisation cookie to use authCookie = generateCookie(); // Initialize the msnftp connection class msnFtpConnection_ = new MsnFtpConnection( getContactHandle(), authCookie ); connectMsnFtpConnection(); // Get the IP,port to use portString = QString::number( msnFtpConnection_->getLocalServerPort() ); // Initialize the progress dialog initializeProgressDialog(); // Set the first status message. slotMsnFtpStatusMessage( i18n("Negotiating options to connect"), MsnFtpConnection::STATUS_DIALOG ); // Create the invitation message // TODO: PortX-Internal and PortX should be another port. // We should be listening to port 11178 as well for internal transfers MimeMessage response; response.addField( "Invitation-Command", "ACCEPT" ); response.addField( "Invitation-Cookie", getCookie() ); response.addField( "IP-Address", getExternalIp() ); // response.addField( "IP-Address-Internal", getLocalIp() ); // response.addField( "PortX-Internal", portString ); response.addField( "Port", portString ); // response.addField( "PortX", portString ); response.addField( "Launch-Application", "FALSE" ); response.addField( "AuthCookie", authCookie ); sendMessage( response ); } // Connect to the port and start listening for the transfer. 00839 void FileTransfer::userStarted3_UserPrepares() { #ifdef KMESSTEST KMESS_ASSERT( msnFtpConnection_ != 0 ); KMESS_ASSERT( file_ != 0 ); #endif if(file_ != 0) { msnFtpConnection_->sendFile(file_); } else { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kDebug() << "Pointer file_ is null!"; #endif slotMsnFtpTransferFailed(); } } #include "filetransfer.moc"