/*************************************************************************** filetransferp2p.cpp - description ------------------- begin : Sun 12 19 2004 copyright : (C) 2004 by Diederik van der Boor email : 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 "filetransferp2p.h" #include "../../utils/thumbnailprovider.h" #include "../../utils/kmessshared.h" #include "../../utils/kmessconfig.h" #include "../../contact/msnobject.h" #include "../../dialogs/transferwindow.h" #include "../../kmessdebug.h" #include "../mimemessage.h" #include "../p2pmessage.h" #include <QFile> #include <QFileInfo> #include <QRegExp> #include <QTextDocument> // for Qt::escape() #include <KFileDialog> #include <KIconLoader> #include <KLocale> #include <KMessageBox> #include <KUrl> /** * The constructor for the FileTransferP2P class, without filename (sufficient for incoming sessions) * * @param applicationList The shared sources for the contact. */ 00047 FileTransferP2P::FileTransferP2P(ApplicationList *applicationList) : P2PApplication(applicationList), file_(0), fileSize_(0), thumbnailProvider_(0), transferID_(-1) { setApplicationType( ChatMessage::TYPE_APPLICATION_FILE ); } /** * The constructor for the FileTransferP2P, with filename to start a session * * @param applicationList The shared sources for the contact. * @param filename Filename of the file to send. */ 00065 FileTransferP2P::FileTransferP2P(ApplicationList *applicationList, const QString &filename) : P2PApplication(applicationList), file_(0), filePath_(filename), fileSize_(0), thumbnailProvider_(0), transferID_(-1) { setApplicationType( ChatMessage::TYPE_APPLICATION_FILE ); } /** * Destructor, closes the file if it's open. */ 00081 FileTransferP2P::~FileTransferP2P() { // Displays the cancel state in the transfer window TransferWindow::getInstance()->failTransfer( transferID_ ); // Close the file // Delete thumbnail provider. delete file_; delete thumbnailProvider_; } /** * The contact cancelled the session */ 00096 void FileTransferP2P::contactAborted(const QString &message) { // Displays the cancel state in the transfer window TransferWindow::getInstance()->failTransfer( transferID_, i18n("Cancelled") ); // Continue with standard call. P2PApplication::contactAborted( message ); } /** * Step one of a contact-started chat: the contact invites the user * * @param message The invitation message */ 00112 void FileTransferP2P::contactStarted1_ContactInvitesUser(const MimeMessage &message) { #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug(); #endif // First stage of the file transfer: initiate the session // Read the values from the message unsigned long int appID = message.getValue("AppID").toUInt(); QString context( message.getValue("Context") ); if(appID != 2) { kmWarning() << "Received unexpected AppID: " << appID << "."; // Wouldn't know what to do if the AppID is not 2, so send an 500 Internal Error back. showEventMessage( i18n("The file transfer invitation was cancelled. Bad data was received."), ChatMessage::CONTENT_APP_CANCELED, true ); sendCancelMessage( CANCEL_ABORT ); return; } // The context field contains file transfer data. QByteArray decodedContext = QByteArray::fromBase64( context.toLatin1() ); // Just to be on the safe side, check the buffer size before we start copying. if( context.length() <= 24 ) { kmWarning() << "File transfer context field has bad formatting, " "ignoring invite (context=" << context << ", contact=" << getContactHandle() << ")."; showEventMessage( i18n("The file transfer invitation was cancelled. Bad data was received."), ChatMessage::CONTENT_APP_CANCELED, true ); sendCancelMessage( CANCEL_ABORT ); return; } // Extract the simple fields from the context string. unsigned int fieldsLength = P2PMessage::extractBytes(decodedContext, 0); // field 1: length of fields 1-6 //unsigned int unknown = P2PMessage::extractBytes(decodedContext, 4); // field 2: unknown. (usually 2) fileSize_ = P2PMessage::extractBytes(decodedContext, 8); // field 3: file size unsigned int noPreview = P2PMessage::extractBytes(decodedContext, 16); // field 4: 1 if NO preview available. // Again, I don't want KMess to crash or being exploited. if( fieldsLength > (uint) decodedContext.size() || fieldsLength < 24 ) { kmWarning() << "File transfer context field has bad formatting, rejecting invite" " (length=" << fieldsLength << ", contact=" << getContactHandle() << ")"; showEventMessage( i18n("The file transfer invitation was cancelled. Bad data was received."), ChatMessage::CONTENT_APP_CANCELED, true ); sendCancelMessage( CANCEL_ABORT ); return; } // field 6: most likely a splitter mark between the file name and preview fields. It's always 0xFFFFFFFF /* // TODO: fix detection of the splitter message. unsigned in splitterPos = qMin( 570, fieldsLength - 4 ); unsigned int splitter = P2PMessage::extractBytes( decodedContext, splitterPos ); if( splitter != 0xFFFFFFFF ) { kmWarning() << "File transfer context field has bad formatting " "(splitter not found at byte " << splitterPos << "," " found 0x" << QString::number( splitter, 16 ) << " contact=" << getContactHandle() << ")."; // sendCancelMessage( CANCEL_ABORT ); // return; } */ // field 5: the file name //unsigned int filenameLength = (fieldsLength - 40); // 24 bytes = 4 dwords, 1 qword (fields 1-4 and 6.) void *pointer = decodedContext.data() + 20; suggestedFileName_ = QString::fromUtf16( (unsigned short*) pointer ); // After field 6: the preview data. QByteArray rawPreviewData; QString previewData; if( noPreview ) { // Generate a default preview icon // The "preview_" value is displayed later in the transfer window too. KIconLoader *loader = KIconLoader::global(); QString iconTitle ( KMimeType::iconNameForUrl( KUrl( suggestedFileName_ ) ) ); preview_ = QImage( loader->iconPath( iconTitle, 48, false ) ); // Save as PNG for encoding QBuffer buffer( &rawPreviewData ); buffer.open( QIODevice::WriteOnly ); preview_.save( &buffer, "PNG" ); // Encode to Base64 for inline image. previewData = rawPreviewData.toBase64(); #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "Using icon '" << iconTitle << "' as preview."; #endif } else { // Determine position of the preview data. const char *previewStart = decodedContext.data() + fieldsLength; const int previewLength = decodedContext.size() - fieldsLength; QByteArray rawPreviewData = QByteArray::fromRawData( previewStart, previewLength ); // wrap, does not copy // Read preview data previewData = rawPreviewData.toBase64(); preview_ = QImage::fromData( rawPreviewData ); #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "Encoded data size: " << previewData.length(); #endif } // Everything seams OK, allow the user to accept the file. // Generate accept message QString html( i18n( "The contact wants to send you a file: "%1" (%2).", "<span class=\"filename invitationFilename\">" + suggestedFileName_ + "</span>", toReadableBytes( fileSize_ ) ) ); // Add preview to the top if( ! noPreview ) { html.prepend( "<img src=\"data:image/png;base64," + previewData + "\"" " width=\"" + QString::number( preview_.width() ) + "\"" " height=\"" + QString::number( preview_.height() ) + "\"" " alt=\"" + Qt::escape( suggestedFileName_ ) + "\" />" "<br />" ); } // Display the accept message. #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "File to transfer is " << suggestedFileName_ << ", " << "waiting for user to accept."; #endif offerAcceptOrReject( html ); } /** * Step two of a contact-started chat: the user accepts. */ 00262 void FileTransferP2P::contactStarted2_UserAccepts() { #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "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() ) { QString suffix; int nextNumber = 0; fileInfo.setFile( suggestedFileName_ ); if( KMessShared::selectNextFile( startFolder, fileInfo.baseName(), suffix, fileInfo.completeSuffix(), nextNumber ) ) { // The file already exists, choose the next one filePath_ = startFolder + fileInfo.baseName() + "." + QString::number( nextNumber ) + "." + fileInfo.completeSuffix(); } else { // New file, use the suggested name filePath_ = startFolder + suggestedFileName_; } 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 kmDebug() << "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 kmDebug() << "Cancelling session"; #endif // Notify the user, even if debug mode is not enabled. kmWarning() << "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 the file "%1" failed. Could not save the file.", "<span class=\"filename failedFilename\">" + fileName_ + "</span>"), ChatMessage::CONTENT_APP_FAILED, false ); // Send 500 Internal Error back if we failed (this is still the accept stage) sendCancelMessage(CANCEL_FAILED); return; } #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "Sending accept message"; #endif // Initialize the progress dialog initializeProgressDialog(true, fileSize_); showTransferMessage( i18n("Negotiating options to connect") ); // Create the message MimeMessage message; message.addField( "SessionID", QString::number( getInvitationSessionID() ) ); // Send the ACCEPT message sendSlpOkMessage(message); // Notify the user modifyOfferMessage(); showEventMessage( i18n( "You have accepted the transfer of the file "%1".", fileName_ ), ChatMessage::CONTENT_APP_STARTED, false ); } /** * Step three of a contact-started chat: the contact confirms the accept * * @param message The message of the other contact, not usefull in P2P sessions because it's an ACK. */ 00416 void FileTransferP2P::contactStarted3_ContactConfirmsAccept(const MimeMessage &/*message*/) { #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug(); #endif // When the 200 OK message is accepted, the contact sends // another invitation with a different content-type, and the // contactStarted1_ContactInvitesUser() is called again. // The second time, the contact initiates the file transfer based // on the fields we sent in the second 200 OK message. // Once that message is received, the gotData() handles the rest. } /** * Step four in a contact-started chat: the contact confirms the data preparation message. */ 00436 void FileTransferP2P::contactStarted4_ContactConfirmsPreparation() { #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug(); #endif // NOTE: with WLM at slow computers, the contact actually started // to send data before the transfer was confirmed. be careful here.. } /** * @brief Create the context field. * * This field is used in the invitation message. * * @param fileData The handle to the opened file. * @param usePreview Whether to use the preview in thumbnailProvider_ */ 00456 QString FileTransferP2P::createContextField( const QFile *fileData, bool usePreview ) const { // Get the thumbnail results. bool hasPreview = ( usePreview && thumbnailProvider_ != 0 && thumbnailProvider_->isSuccessful() ); QByteArray previewData; // Only generate preview if requested and object exists. if( hasPreview ) { previewData = thumbnailProvider_->getData(); } #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "Creating context field " << ( usePreview ? "with thumbnail." : "without thumbnail." ); #endif // Get the file data and preview quint32 filesize = (quint32) fileData->size(); int flags = (hasPreview ? 0 : 1); // Determine the context length int length = 574; // Somehow MSN always uses 550 bytes for the filename (+24 for the size of fields 1-6). int totalLength = length + previewData.size(); // Fill the context parameter QByteArray context( totalLength, 0x00 ); P2PMessage::insertBytes( context, length, 0 ); // Field 1: Length of fields 1-6 P2PMessage::insertBytes( context, 2, 4 ); // Field 2: somehow this is always 2 P2PMessage::insertBytes( context, filesize, 8 ); // Field 3: file size (QWord) P2PMessage::insertBytes( context, flags, 16 ); // Field 4: 1 if NO preview data // Field 5: the file name P2PMessage::insertUtf16String( context, fileName_, 20 ); P2PMessage::insertBytes(context, 0xFFFFFFFF, 570); // Field 6: some splitter field. // Insert preview data if( hasPreview ) { context.replace( 574, previewData.size(), previewData ); } // Encode the context QByteArray encodedContext = context.toBase64(); QString base64Context( QString::fromUtf8( encodedContext.data(), encodedContext.size() ) ); #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "Context size=" << context.size() << " base64 size=" << base64Context.length() << "."; #endif // Return it. return base64Context; } /** * End the application with another message in the file transfer dialog as well. */ 00516 void FileTransferP2P::endApplication() { QString html; #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug(); #endif // Mark the transfer as failed and terminate. TransferWindow::getInstance()->failTransfer( transferID_ ); P2PApplication::endApplication(); } /** * Return the application's GUID. */ 00535 QString FileTransferP2P::getAppId() { return "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"; } /** * Return a cancel message to display. */ 00545 QString FileTransferP2P::getContactAbortMessage() const { // Application::getContactAbortMessage() returns "The contact cancelled the session". return i18n("The contact has cancelled the transfer of the file "%1".", ( fileName_.isEmpty() ? suggestedFileName_ : fileName_ ) ); } /** * Return a cancel message to display. */ 00556 QString FileTransferP2P::getUserAbortMessage() const { // Application::getUserAbortMessage() returns "You have cancelled the session". return i18n( "You have cancelled the transfer of the file "%1".", ( fileName_.isEmpty() ? suggestedFileName_ : fileName_ ) ); } /** * Return a cancel message to display. */ 00567 QString FileTransferP2P::getUserRejectMessage() const { // Application::getUserAbortMessage() returns "You have cancelled the session". return i18n( "You have rejected the transfer of the file "%1".", ( fileName_.isEmpty() ? suggestedFileName_ : fileName_ ) ); } /** * Called when data is received. * Once all data is received, the SLP BYE message will be sent. * * @param message P2P message with the data. */ 00581 void FileTransferP2P::gotData(const P2PMessage &message) { if(file_ == 0) { kmWarning() << "Unable to handle file data: no file open or already closed " << "(offset=" << message.getDataOffset() << " totalsize=" << message.getDataSize() << " contact=" << getContactHandle() << ")!"; // Cancel if we can't receive it. // If this happens we're dealing with a very stubborn client, // because we already rejected the data-preparation message. sendCancelMessage(CANCEL_FAILED); return; } #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "Data part received, saving data to file."; #endif // Write the data in the file // Let the parent class do the heavy lifting, and abort properly. bool success = writeP2PDataToFile( message, file_ ); if( ! success ) { // Close the file file_->flush(); file_->close(); delete file_; file_ = 0; // Display the failure in the transfer window TransferWindow::getInstance()->failTransfer( transferID_, i18n("File could not be written") ); return; } // When all data is received, the parent class calls showTransferComplete(). } /** * Create and initilize the progress dialog. * * @param incoming Set to indicate whether this is an incoming file transfer or not. * @param filesize Size of the file to transfer. */ 00629 void FileTransferP2P::initializeProgressDialog(bool incoming, uint filesize) { // Create a new entry in the tranfer window TransferWindow *transferWindow = TransferWindow::getInstance(); transferID_ = transferWindow->addEntry( filePath_, filesize, incoming, preview_ ); // Connect the dialog so that if the user closes it, it's deleted. connect( transferWindow, SIGNAL( cancelTransfer(int) ) , this, SLOT( slotCancelTransfer(int) ) ); } /** * Called when the transfer is complete. * This function is also called from the P2PApplication base class. * The application will terminate automatically. */ 00647 void FileTransferP2P::showTransferComplete() { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kmDebug() << "Transfer is complete."; #endif if(file_ != 0) { // Clean up file_->flush(); file_->close(); delete file_; file_ = 0; } // 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 the file "%1".", fileURL ); } else { message = i18n( "Successfully received the file "%1".", fileURL ); } showEventMessage( message, ChatMessage::CONTENT_APP_ENDED, ! isUserStartedApp() ); } /** * Show a message to inform about a transfer event (shown in the transfer dialog, e.g. connecting to host). * This function is also called from the P2PApplication base class. */ 00689 void FileTransferP2P::showTransferMessage(const QString &message) { // Display the message in the transfer window TransferWindow::getInstance()->setStatusMessage( transferID_, message ); } /** * Show the progress made during a transfer. * This function is also called from the P2PApplication base class. */ 00701 void FileTransferP2P::showTransferProgress(const ulong bytesTransferred) { // Display the progress update in the transfer window TransferWindow::getInstance()->updateProgress( transferID_, bytesTransferred ); } /** * Cancelled the file transfer from the TransferWindow */ 00712 void FileTransferP2P::slotCancelTransfer( int transferID ) { // Check if the cancelled transfer is ours if( transferID_ != transferID ) { return; } #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kmDebug(); #endif // Use the same route as closing the chat window. userAborted(); // Now we wait for the contact to end the session } /** * Convert a string to some more readable form */ 00734 QString FileTransferP2P::toReadableBytes(uint 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 */ 00759 void FileTransferP2P::userAborted() { #ifdef KMESSDEBUG_FILETRANSFER_GENERAL kmDebug(); #endif // Display the cancel state in the transfer window TransferWindow::getInstance()->failTransfer( transferID_, i18n("Cancelled") ); // Let the parent class handle the rest P2PApplication::userAborted(); } /** * Step one of a user-started chat: the user invites the contact */ 00777 void FileTransferP2P::userStarted1_UserInvitesContact() { #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug() << "starting file transfer"; #endif #ifdef KMESSTEST KMESS_ASSERT( file_ == 0 ); #endif /* * Many thanks to Siebe Tolsma for providing this documentation: * http://siebe.bot2k3.net/docs/ */ 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 kmDebug() << "Cancelling session"; #endif // Notify the user, even if debug mode is not enabled. kmWarning() << "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 the 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 the 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 filename parameters fileSize_ = (uint)file_->size(); // Create a thumbnail, continue when it completes // Windows Live Messenger scales the received image down depending on the users settings. // The preview however, is always sent as 96x96 thumbnailProvider_ = new ThumbnailProvider( filePath_, 96 ); connect( thumbnailProvider_, SIGNAL(gotResult()), this, SLOT(userStarted1_gotThumbnailResult()) ); } /** * @brief Called when the thumbnail is generated. */ 00841 void FileTransferP2P::userStarted1_gotThumbnailResult() { bool hasPreview = thumbnailProvider_->isSuccessful(); // Send the invitation QString context( createContextField( file_, hasPreview ) ); sendSlpSessionInvitation( KMessShared::generateID(), getAppId(), 2, context ); // Generate the HTML to cancel the transfer QString html( i18n( "Sending file "%1" (%2).", "<span class=\"filename invitationFilename\">" + fileName_ + "</span>", toReadableBytes( fileSize_ ) ) ); // Generate fallback image so there is always an icon in the chat window. if( ! hasPreview ) { thumbnailProvider_->generateFallbackImage(); hasPreview = thumbnailProvider_->isSuccessful(); } // Insert the preview image if this is available. if( hasPreview ) { html = thumbnailProvider_->getImageTag( fileName_ ) + "<br />" + html; preview_ = thumbnailProvider_->getImage(); // Save for transfer dialog later. } // Display the link in the chat window offerCancel( html ); } /** * Step two of a user-started chat: the contact accepts * * @param message Accept message of the other contact */ 00880 void FileTransferP2P::userStarted2_ContactAccepts(const MimeMessage & /*message*/) { #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug(); #endif // Update the GUI modifyOfferMessage(); showEventMessage( i18n( "The contact has accepted the transfer of the file "%1".", fileName_ ), ChatMessage::CONTENT_APP_STARTED, true ); initializeProgressDialog(false, fileSize_); // Send the invite to negatiate the transfer mode. sendSlpTransferInvitation(); } /** * Step three of a user-started chat: the user prepares for the session. */ 00902 void FileTransferP2P::userStarted3_UserPrepares() { #ifdef KMESSDEBUG_FILETRANSFER_P2P kmDebug(); #endif #ifdef KMESSTEST KMESS_ASSERT( file_ != 0 ); #endif // A connection is available to send the file. // The base class handles the transfer transparently (e.g. using direct connections, etc). sendData( file_, P2P_TYPE_FILE ); } #include "filetransferp2p.moc"