/*************************************************************************** kmessshared.cpp - description ------------------- begin : Thu May 8 2008 copyright : (C) 2008 by Valerio Pilo email : valerio@kmess.org ***************************************************************************/ /*************************************************************************** * * * 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 "kmessshared.h" #include "../kmess.h" #include "../kmessapplication.h" #include "../kmessdebug.h" #include "../currentaccount.h" #include "../network/soap/passportloginservice.h" #include "kmessconfig.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef KMESSDEBUG_SHAREDMETHODS #define KMESSDEBUG_SHARED_OPENEXTERNAL #endif // Create a HMACSha1 hash QByteArray KMessShared::createHMACSha1 ( const QByteArray &keyForHash , const QByteArray &secret ) { // The algorithm is definited into RFC 2104 int blocksize = 64; QByteArray key( keyForHash ); QByteArray opad( blocksize, 0x5c ); QByteArray ipad( blocksize, 0x36 ); // If key size is too larg, compute the hash if( key.size() > blocksize ) { key = QCryptographicHash::hash( key, QCryptographicHash::Sha1 ); } // If too small, pad with 0x00 if( key.size() < blocksize ) { key += QByteArray( blocksize - key.size() , 0x00 ); } // Compute the XOR operations for(int i=0; i < key.size() - 1; i++ ) { ipad[i] = (char) ( ipad[i] ^ key[i] ); opad[i] = (char) ( opad[i] ^ key[i] ); } // Append the data to ipad ipad += secret; // Compute result: hash sha1 of ipad and append the data to opad opad += QCryptographicHash::hash( ipad, QCryptographicHash::Sha1 );; // Return array contains the result of HMACSha1 return QCryptographicHash::hash( opad, QCryptographicHash::Sha1 ); } // Create the html file to request the given service QString KMessShared::createRequestFile( const QString &mailto, const QString &folder ) { CurrentAccount *currentAccount = CurrentAccount::instance(); // Use one file for hml page with post method QString filename( KMessConfig::instance()->getAccountDirectory( currentAccount-> getHandle() ) + "/hotmail.htm" ); QFile file( filename ); if ( ! file.open( QIODevice::WriteOnly | QIODevice::Text ) ) { return QString(); } QString command( folder ); // Check if the user wants to send an email if( ! mailto.isEmpty() ) { command += "?mailto=1&to=" + mailto; } // Use the method into passport to compute the token QString token( PassportLoginService::createHotmailToken( currentAccount->getToken( "Passport" ), currentAccount->getToken( "PassportProof" ), command ) ); QTextStream outBuffer( &file ); // Write the file contents outBuffer << "\n" "\n" " \n" " \n" "
getLanguageCode() + "\" method=\"POST\">\n" " \n" " \n" "
\n" " \n" "\n"; // Then close the buffer file.close(); return filename; } // Return a derived key with HMACSha1 algorithm QByteArray KMessShared::deriveKey ( const QByteArray &keyToDerive, const QByteArray &magic ) { // create the four hashes needed for the implementation. QByteArray hash1, hash2, hash3, hash4, temp; QByteArray key(keyToDerive); // append the magic string. temp.append ( magic ); // create the HMAC Sha1 hash and assign it. hash1 = createHMACSha1 ( key, temp ); temp.clear(); // append the first hash with the magic string. temp.append ( hash1 + magic ); // create the HMAC Sha1 hash and assign it. hash2 = createHMACSha1 ( key, temp ); temp.clear(); // append the first hash. temp.append ( hash1 ); // create the HMAC Sha1 hash and assign it. hash3 = createHMACSha1 ( key, temp ); temp.clear(); // assign the third hash with the magic string. temp.append ( hash3 + magic ); // create the HMAC Sha1 hash and assign it. hash4 = createHMACSha1 ( key, temp ); temp.clear(); // return the second hash with the first four bytes of the fourth hash. return hash2 + hash4.left ( 4 ); } /** * @brief Draws an icon with an overlay from two pixmaps. * * This method takes the two pixmaps and draws them over each other, returning a new pixmap with 'overlay' * as an overlay, with given width and height. If -1 are given as width and height, the size of 'icon' * is taken as the end size. -1 is the default for both width and height. * * Note that the icon always sticks to the top left, whatever the size is, and the overlay always sticks to * the bottom right. * * @param icon The icon which will be displayed as the "main image". * @param overlay The overlay which will be displayed over the icon. * @param width Width of the returned icon. * @param height Height of the returned icon. * @param sizeFactor the size of the overlay, compared with the size of the result image; if 1, it fills the image, if .5, the overlay is half the size. * @param iconSizeFactor the size of the icon, compared with the size of the result image; if 1, it fills the image (background), if .5, it's half the size. * @returns the icon with the overlay overlayed on it. */ QPixmap KMessShared::drawIconOverlay( const QPixmap icon, const QPixmap overlay, int width, int height, float sizeFactor, float iconSizeFactor ) { if( width == -1 ) { width = icon.size().width(); } if( height == -1 ) { height = icon.size().height(); } const int imageWidth = width * iconSizeFactor; const int imageHeight = height * iconSizeFactor; const int overlayWidth = width * sizeFactor; const int overlayHeight = height * sizeFactor; const int x = width - overlayWidth; const int y = height - overlayHeight; // thanks, Amarok, for the code and the idea :) QPixmap result( width, height ); result.fill( Qt::transparent ); QPainter p( &result ); p.drawPixmap( 0, 0, icon. scaled( imageWidth, imageHeight ) ); p.drawPixmap( x, y, overlay.scaled( overlayWidth, overlayHeight ) ); p.end(); return result; } /** * @brief Generate a random GUID. * * This is used in MSNP2P for the Call ID and Branch ID. * * @return A randomly generated GUID value. */ QString KMessShared::generateGUID() { // much easier than the code below :) return QUuid::createUuid().toString().toUpper(); /* // This code is based on Kopete, but much shorter. QString guid( "{" + QString::number((rand() & 0xAAFF) + 0x1111, 16) + QString::number((rand() & 0xAAFF) + 0x1111, 16) + "-" + QString::number((rand() & 0xAAFF) + 0x1111, 16) + "-" + QString::number((rand() & 0xAAFF) + 0x1111, 16) + "-" + QString::number((rand() & 0xAAFF) + 0x1111, 16) + "-" + QString::number((rand() & 0xAAFF) + 0x1111, 16) + QString::number((rand() & 0xAAFF) + 0x1111, 16) + QString::number((rand() & 0xAAFF) + 0x1111, 16) + "}" ); return guid.toUpper(); */ } /** * @brief Generate an random number to use as ID. * * For use in MSNP2P, the value will not be below 4 * * @return A random value between 4 and RAND_MAX. */ quint32 KMessShared::generateID() { //return (rand() & 0x00FFFFFC); // seed the RNG properly. srand(time(NULL)); return (rand() + 100); // at least 3 digits for an ID (as per ticket #294??). } // Return the hash of a file name using the SHA1 algorithm QByteArray KMessShared::generateFileHash( const QString &fileName ) { // Read the file's contents into a byte array QFile file( fileName ); if( ! file.open( QIODevice::ReadOnly ) ) { return QByteArray(); } QByteArray fileData( file.readAll() ); file.close(); // Retrieve and return the file's hash return QCryptographicHash::hash( fileData, QCryptographicHash::Sha1 ); } /** * Converts a string with HTML to one with escaped entities * * Only the main HTML control characters are escaped; the string is made suitable for * insertion within an HTML tag attribute. * Neither KDE nor Qt have escape/unescape methods this thorough, they only replace the <>&. Annoying. * * @param string The string to modify * @return A reference to the modified string */ QString &KMessShared::htmlEscape( QString &string ) { string.replace( ";", ";" ) .replace( "&", "&" ) .replace( "&#59;", ";" ) // oh god :( .replace( "<", "<" ) .replace( ">", ">" ) .replace( "'", "'" ) .replace( '"', """ ); // NOTE: by not using " this result is also usable for XML. return string; } /** * Converts a string constant with HTML to one with escaped entities * * This version is suitable for use with string constants, as it creates a copy of the original string. * * @param string The string to modify * @return Another string with the escaped contents */ QString KMessShared::htmlEscape( const QString &string ) { QString copy( string ); return htmlEscape( copy ); } /** * Converts a string with escaped entities to one with HTML * * Only the main HTML control characters are unescaped; the string is reverted back to its * original state. * Neither KDE nor Qt have HTML to text decoding methods. Annoying. * * @param string The string to modify * @return A reference to the modified string */ QString &KMessShared::htmlUnescape( QString &string ) { string.replace( """, "\"" ) .replace( "'", "'" ) .replace( ">", ">" ) .replace( "<", "<" ) .replace( ";", "&#59;" ) .replace( "&", "&" ) .replace( ";", ";" ); return string; } /** * Converts a string with escaped entities to one with HTML * * This version is suitable for use with string constants, as it creates a copy of the original string. * * @param string The string to modify * @return Another string with the escaped contents */ QString KMessShared::htmlUnescape( const QString &string ) { QString copy( string ); return htmlUnescape( copy ); } /** * @brief Open the given URL respecting the user's preference * * The program which will open the URL is the one selected in the Global Settings * * @param url The URL to open */ void KMessShared::openBrowser( const KUrl &url ) { // Open the options KConfigGroup group = KMessConfig::instance()->getGlobalConfig( "General" ); QString browser; QString fallbackBrowser( group.readEntry( "customBrowser", "konqueror" ) ); // Obtain the widget of the main KMess window, to correctly link it to the opened browser KMessApplication *kmessApp = static_cast( kapp ); QWidget *mainWindow = kmessApp->getContactListWindow()->window(); // Read which browser has to be opened QString browserChoice( group.readEntry( "useBrowser", "KDE" ) ); #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "Opening URL" << url << "- The setting is" << browserChoice; #endif // If the preference had an invalid choice, use the KDE default browser. if( browserChoice != "custom" && browserChoice != "listed" && browserChoice != "KDE" ) { kWarning() << "Invalid browser choice:" << browserChoice << ", using KDE default."; browserChoice = "KDE"; } // Launch the appropriate browser if( browserChoice == "custom" ) { browser = group.readEntry( "customBrowser", fallbackBrowser ); #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "Custom browser selected:" << browser; #endif } else if( browserChoice == "listed" ) { browser = group.readEntry( "listedBrowser", fallbackBrowser ); #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "Listed browser selected:" << browser; #endif } // If the option isn't valid use the KDE default if( browser.isEmpty() ) { #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "Invalid browser option, using KDE default."; #endif browserChoice = "KDE"; } if( browserChoice == "KDE" ) { #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "KDE default browser selected."; #endif KToolInvocation::invokeBrowser( url.url() ); return; } // Replace the %u in the command line with the actual URL, and launch the browser. browser.replace( "%u", KShell::quoteArg( url.url() ), Qt::CaseInsensitive ); KRun::runCommand( browser, mainWindow ); } /** * @brief Open the given URL mail respecting the user's preference * * The program which will open the URL is the one selected in the Global Settings * * @param mailto The address to compose a message to (can also be empty to open the folder instead). * @param folder The folder to open. * @param isHotmailAvailable Whether Hotmail can be used to access the email for this account. */ void KMessShared::openEmailClient( const QString &mailto, const QString &folder, bool isHotmailAvailable ) { // Open the options KConfigGroup group = KMessConfig::instance()->getGlobalConfig( "General" ); // When using Live! Mail accounts, the webmail can be used if( isHotmailAvailable && group.readEntry( "useLiveMail", true ) ) { // Create the request file QString filename( createRequestFile( mailto, folder ) ); if( ! filename.isEmpty() ) { // Open the webmail in the browser openBrowser( KUrl( "file://" + filename ) ); } return; } QString mailClient; QString fallbackMailClient( group.readEntry( "customMailClient", "kmail" ) ); // Obtain the widget of the main KMess window, to correctly link it to the opened app KMessApplication *kmessApp = static_cast( kapp ); QWidget *mainWindow = kmessApp->getContactListWindow()->window(); // Read which mail client has to be opened QString mailClientChoice( group.readEntry( "useMailClient", "KDE" ) ); #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "Opening mail at" << folder << "- The setting is" << mailClientChoice; #endif // If the preference had an invalid choice, use the KDE default mail client. if( mailClientChoice != "custom" && mailClientChoice != "listed" && mailClientChoice != "KDE" ) { kWarning() << "Invalid mail client choice:" << mailClientChoice << ", using KDE default."; mailClientChoice = "KDE"; } // Launch the appropriate mail client if( mailClientChoice == "custom" ) { mailClient = group.readEntry( "customMailClient", fallbackMailClient ); #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "Custom mail client selected:" << mailClient; #endif } else if( mailClientChoice == "listed" ) { mailClient = group.readEntry( "listedMailClient", fallbackMailClient ); #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "Listed mail client selected:" << mailClient; #endif } // If the option isn't valid use the KDE default if( mailClient.isEmpty() ) { #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "Invalid mail client option, using KDE default."; #endif mailClientChoice = "KDE"; } // Retrieve the mail address to open if( mailClientChoice == "KDE" ) { #ifdef KMESSDEBUG_SHARED_OPENEXTERNAL kDebug() << "KDE default mail client selected."; #endif KToolInvocation::invokeMailer( KUrl( "mailto:" + mailto ) ); return; } // Replace the %u in the command line with the contact address, and launch the application. mailClient.replace( "%u", KShell::quoteArg( mailto ), Qt::CaseInsensitive ); KRun::runCommand( mailClient, mainWindow ); } /** * Select the next available file. * * This method searches for numbered files and tries to find the last *existing* file. * If it returns false, ( baseName + "." + extension ) can be created directly; otherwise, * ( baseName + suffix + "." + extension ) is the last file that does exist and to create * a new file, use ( baseName + "." + startingNumber + "." + extension ). * * If startingNumber is 1, there is actually no suffix; but this is never a problem if you * correctly handle this method returning false, reading the existing file using the suffix, * and writing a new file using startingNumber. * * For example, calling selectNextFile( "/", "name", suffix, "ext", startingNumber ): * - if startingNumber is zero and "/name.ext" doesn't exist, returns false, * suffix = "", startingNumber varies depending on if anything after name.ext exists * (ie if name.1.ext exists but name.2.ext doesn't, startingNumber = 2) * - else if "/name.1.ext" doesn't exist, returns true, suffix = "", startingNumber = 1 * (so the last existing file will be "name.ext", which is true) * - else if "/name.2.ext" doesn't exist, returns true, suffix = ".1", startingNumber = 2 * - and so forth * * // Create a new unique file: int startingNumber = 0; QString suffix; if( KMessShared::selectNextFile( "/kmess/data/directory/", "logFile", suffix, "html", startingNumber ) ) { // The base name exists, create a numbered file path = "/kmess/data/directory/logFile." + QString::number( startingNumber ) . ".html"; } else { // The base name doesn't exist, use it instead path = "/kmess/data/directory/logFile.html"; } * @param directory (in) Path where the files will be searched, followed by '/'. * May be relative. * @param baseName (in) Common name for the files, without the extension, see above. * @param suffix (out) Append to the baseName to get the name of the last * existing numbered file. * @param extension (in) Extension of the file to search for, see above. * @param startingNumber (in/out) File number to start from, set to zero to start with * the common name alone. Gets updated with the number of * the first available file. * @return bool If the file in the updated fileName param exists or not. */ bool KMessShared::selectNextFile( const QString &directory, const QString &baseName, QString &suffix, const QString &extension, int &startingNumber ) { QString path( directory + baseName + "." ); // if startingNumber is nonzero, we never allow to return the base file // this is important if the base file exists but .1.xml doesn't exist; // if this boolean is true it will return .1.xml and return false, if it's // false it will be allowed to return .xml and return true. // HACK XXX FIXME - Fix this damn method for 2.0! We can't have this legacy code like this. bool returnBaseFileAllowed = startingNumber == 0; if( returnBaseFileAllowed && ! QFile::exists( path + extension ) ) { // "baseName.ext" doesn't exist, so return false // but first find the next available file for writing while( QFile::exists( path + QString::number( ++startingNumber ) + "." + extension ) ) ; #ifdef KMESSDEBUG_SHAREDMETHODS kDebug() << "Base file " << ( baseName + "." + extension) << "does not exist. Next sequential file is: " << ( baseName + "." + QString::number( startingNumber ) + "." + extension ); #endif suffix = ""; return false; } #ifdef KMESSDEBUG_SHAREDMETHODS kDebug() << "Base file" << ( baseName + "." + extension ) << "exists, finding the last existing numbered file"; #endif // Start the search sequence number from 1 if( startingNumber < 1 ) { startingNumber = 1; } // find the last existing file while( QFile::exists( path + QString::number( startingNumber ) + "." + extension ) ) { startingNumber++; } // Found the first non-existing file if( startingNumber == 1 ) { if( returnBaseFileAllowed ) { // the last existing file is the base filename, without a suffix suffix = QString(); } else { suffix = ".1"; #ifdef KMESSDEBUG_SHAREDMETHODS kDebug() << "Called with nonzero startingNumber, so I'm not allowed to return the base file!" << "Returning " << ( baseName + suffix + "." + extension ) << " which doesn't exist."; #endif return false; } } else { // the last existing file is the previously checked file in the loop suffix = "." + QString::number( startingNumber - 1 ); } #ifdef KMESSDEBUG_SHAREDMETHODS kDebug() << "Last existing file name:" << ( baseName + suffix + "." + extension ); #endif return true; } // Truncate an UTF-8 multibyte string to a fixed byte size QString KMessShared::fixStringToByteSize( const QString &string, quint32 size ) { // Check if truncating is not needed if( string.isEmpty() || size == 0 ) { return string; } // Seek through the string one multibyte char at a time, and count their actual size in bytes quint32 validPos = 0; for( quint32 pos = 0; (qint32)pos < string.length(); ++pos ) { QChar character( string.at( pos ) ); quint32 skip = 0; quint32 more; // Character ranges from http://en.wikipedia.org/wiki/UTF-8 if( character <= 0x007f ) { more = 1; } else if( character <= 0x07FF ) { more = 2; } else if( character <= 0xd7ff ) { more = 3; } else if( character <= 0xDFFF ) { // Surrogate area, consume next char as well more = 4; skip = 1; } else { more = 3; } // The next character would make the string too long: cut it here if( validPos + more > size ) { return QString::fromUtf8( string.toUtf8().left( validPos ) ); } validPos += more; pos += skip; } return string; }