Files
kmess/src/utils/kmessshared.cpp
T
2026-07-06 13:14:56 +02:00

1226 lines
35 KiB
C++

/***************************************************************************
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 <ctime>
#include <algorithm>
#include <QUrl>
#include <KShell>
#include <KIO/StoredTransferJob>
#include <QCryptographicHash>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QStandardPaths>
#include <QUuid>
#include <QPainter>
#include <QList>
#include <QString>
#include <QTemporaryFile>
#include <QUrl>
#ifdef KMESSDEBUG_SHAREDMETHODS
#define KMESSDEBUG_SHARED_OPENEXTERNAL
#endif
static QStringList &kmessResourcePrefixes()
{
static QStringList prefixes;
return prefixes;
}
static QStandardPaths::StandardLocation kmessLocationForType( const QString &type )
{
if( type == "config" )
{
return QStandardPaths::GenericConfigLocation;
}
if( type == "xdgdata-apps" )
{
return QStandardPaths::ApplicationsLocation;
}
if( type == "cache" )
{
return QStandardPaths::GenericCacheLocation;
}
if( type == "socket" )
{
return QStandardPaths::RuntimeLocation;
}
if( type == "appdata" )
{
return QStandardPaths::AppDataLocation;
}
return QStandardPaths::GenericDataLocation;
}
static QString kmessResourceSubdir( const QString &type )
{
if( type == "emoticons" )
{
return QStringLiteral( "emoticons" );
}
if( type == "sound" )
{
return QStringLiteral( "sounds" );
}
if( type == "html" )
{
return QStringLiteral( "doc/HTML" );
}
if( type == "xdgdata-icon" )
{
return QStringLiteral( "icons" );
}
if( type == "xdgdata-mime" )
{
return QStringLiteral( "mime" );
}
if( type == "xdgconf-menu" )
{
return QStringLiteral( "menus" );
}
if( type == "xdgconf-autostart" )
{
return QStringLiteral( "autostart" );
}
if( type == "templates" )
{
return QStringLiteral( "templates" );
}
return QString();
}
static QString kmessResourcePath( const QString &type, const QString &path )
{
const QString normalized = QDir::fromNativeSeparators( path );
const QString subdir = kmessResourceSubdir( type );
if( normalized.isEmpty() )
{
return subdir;
}
if( subdir.isEmpty() || normalized.startsWith( subdir + '/' ) )
{
return normalized;
}
return subdir + '/' + normalized;
}
static QStringList kmessPrefixCandidates( const QString &type, const QString &file )
{
QStringList candidates;
const QString mapped = kmessResourcePath( type, file );
foreach( const QString &prefix, kmessResourcePrefixes() )
{
QDir prefixDir( prefix );
candidates << prefixDir.filePath( mapped );
candidates << prefixDir.filePath( "share/" + mapped );
if( type == "data" )
{
candidates << prefixDir.filePath( "share/apps/" + mapped );
}
else if( type == "appdata" )
{
candidates << prefixDir.filePath( "share/kmess/" + file );
}
}
candidates.removeDuplicates();
return candidates;
}
static QStringList kmessSourceCandidates( const QString &type, const QString &file )
{
QStringList candidates;
QString normalized = QDir::fromNativeSeparators( file );
if( type == "data" )
{
if( normalized.startsWith( "kmess/" ) )
{
normalized.remove( 0, 6 );
}
candidates << QDir::current().absoluteFilePath( "data/" + normalized );
}
else if( type == "appdata" )
{
candidates << QDir::current().absoluteFilePath( "data/" + normalized );
}
else if( type == "emoticons" )
{
candidates << QDir::current().absoluteFilePath( "data/emoticons/" + normalized );
if( normalized.startsWith( "KMess-new/" ) )
{
candidates << QDir::current().absoluteFilePath( "data/emoticons/KMess2-new/" + normalized.mid( 10 ) );
}
}
else if( type == "sound" )
{
candidates << QDir::current().absoluteFilePath( "data/sounds/" + normalized );
}
candidates.removeDuplicates();
return candidates;
}
/**
* Sorting helper, to compare two filenames using natural sorting.
*
* Usage of this method:
* @code
qSort( list.first(), list.last(), KMessShared::compareFileNames );
* @endcode
*
* The names are compared naturally: 1 comes before 2, 10 and 20.
* For example, log file names always come in this form: "<contact_handle>[.number].xml"
* and thus natural sorting keeps them in the right temporal order.
*
* @param name1 First filename
* @param name2 Second filename
* @return bool whether the first is lesser than the second
*/
bool KMessShared::compareFileNames( const QString& name1, const QString& name2 )
{
// Compare the raw filename size first: names with less characters naturally come first
if( name1.size() < name2.size() )
{
return true;
}
// Remove the .xml from the filenames: -4 characters to strip from the end of file
const int name1Size = name1.size();
const int name2Size = name2.size();
QString fileName1( name1Size > 4 ? name1.left( name1Size - 4 ) : name1 );
QString fileName2( name2Size > 4 ? name2.left( name2Size - 4 ) : name2 );
// Compare the numbers with a natural sorting by extracting the number from the end of the filename
QString num1( fileName1.mid( fileName1.lastIndexOf( '.' ) + 1 ) );
QString num2( fileName2.mid( fileName2.lastIndexOf( '.' ) + 1 ) );
return ( num1.toInt() < num2.toInt() );
}
// 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();
}
// this will have our (urlencoded) dodgy email address we used to satisfy URL COMPOSE
// at the initial login. replace it where we actually want it to go.
QString command( folder );
command = command.replace( "some.invalid%40kmess.email", 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 << "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\n"
" \"http://www.w3.org/TR/html4/loose.dtd\">\n"
"<html>\n"
" <head></head>\n"
" <body onload=\"document.pform.submit(); \">\n"
" <form name=\"pform\""
"action=\"https://ds.escargot.nina.chat/ppsecure/sha1auth.srf?lc=" +
currentAccount->getLanguageCode() +
"\" method=\"POST\">\n"
" <input type=\"hidden\" name=\"token\" value=\"" +
token +
"\">\n"
" <noscript><p>Please enable JavaScript!</p>\n"
" <input type=\"submit\" value=\"Click here to open Live Mail\"/>\n"
" </noscript>\n"
" </form>\n"
" </body>\n"
"</html>\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 );
}
// Download a URL to a local file path if it is not already local.
bool KMessShared::downloadToLocalFile( const QUrl &url, QString &localFilePath, QString *temporaryFilePath )
{
if( temporaryFilePath != 0 )
{
temporaryFilePath->clear();
}
if( url.isLocalFile() || url.scheme().isEmpty() )
{
localFilePath = url.isLocalFile() ? url.toLocalFile() : url.toString();
return true;
}
QTemporaryFile temporaryFile;
temporaryFile.setAutoRemove( false );
if( ! temporaryFile.open() )
{
return false;
}
KIO::StoredTransferJob *job = KIO::storedGet( url, KIO::NoReload, KIO::HideProgressInfo );
if( ! job->exec() )
{
QFile::remove( temporaryFile.fileName() );
return false;
}
const QByteArray fileData( job->data() );
if( temporaryFile.write( fileData ) != fileData.size() )
{
QFile::remove( temporaryFile.fileName() );
return false;
}
localFilePath = temporaryFile.fileName();
if( temporaryFilePath != 0 )
{
*temporaryFilePath = localFilePath;
}
return true;
}
// Add an application resource prefix used as fallback for relocatable builds.
void KMessShared::addResourcePrefix( const QString &prefix )
{
if( ! prefix.isEmpty() && ! kmessResourcePrefixes().contains( prefix ) )
{
kmessResourcePrefixes().append( prefix );
}
}
// Return the first matching resource path for a KDE resource type.
QString KMessShared::findResource( const QString &type, const QString &file )
{
if( QFileInfo( file ).isAbsolute() )
{
return QFileInfo::exists( file ) ? file : QString();
}
const QString path = kmessResourcePath( type, file );
const QString found = QStandardPaths::locate( kmessLocationForType( type ), path );
if( ! found.isEmpty() )
{
return found;
}
foreach( const QString &candidate, kmessPrefixCandidates( type, file ) )
{
if( QFileInfo::exists( candidate ) )
{
return candidate;
}
}
foreach( const QString &candidate, kmessSourceCandidates( type, file ) )
{
if( QFileInfo::exists( candidate ) )
{
return candidate;
}
}
return QString();
}
// Return a MSN-compatible account handle, adding Escargot's default domain when needed.
QString KMessShared::normalizeMsnHandle( const QString &handle )
{
const QString trimmedHandle( handle.trimmed() );
if( trimmedHandle.contains( '@' ) )
{
return trimmedHandle;
}
return trimmedHandle + "@escargot.chat";
}
// Return the directory containing a matching resource path.
QString KMessShared::findResourceDir( const QString &type, const QString &file )
{
const QString resource = findResource( type, file );
if( resource.isEmpty() )
{
return QString();
}
QString suffix = QDir::fromNativeSeparators( file );
QString resourcePath = QDir::fromNativeSeparators( resource );
if( resourcePath.endsWith( suffix ) )
{
return resource.left( resource.length() - suffix.length() );
}
return QFileInfo( resource ).absolutePath() + '/';
}
// Return all matching resource directories for a KDE resource type.
QStringList KMessShared::findResourceDirs( const QString &type, const QString &path )
{
QStringList dirs = QStandardPaths::locateAll( kmessLocationForType( type ),
kmessResourcePath( type, path ),
QStandardPaths::LocateDirectory );
foreach( const QString &candidate, kmessPrefixCandidates( type, path ) )
{
if( QFileInfo( candidate ).isDir() )
{
dirs.append( candidate );
}
}
foreach( const QString &candidate, kmessSourceCandidates( type, path ) )
{
if( QFileInfo( candidate ).isDir() )
{
dirs.append( candidate );
}
}
dirs.removeDuplicates();
return dirs;
}
// Return the writable local path for a KDE resource type and create its parent directory.
QString KMessShared::locateLocal( const QString &type, const QString &path )
{
const QString mappedPath = kmessResourcePath( type, path );
const QString base = type == "tmp"
? QDir::tempPath()
: QStandardPaths::writableLocation( kmessLocationForType( type ) );
QDir dir( base );
QFileInfo info( mappedPath );
if( info.fileName().isEmpty() || mappedPath.endsWith( '/' ) )
{
dir.mkpath( mappedPath );
return dir.filePath( mappedPath );
}
dir.mkpath( info.path() );
return dir.filePath( mappedPath );
}
// Find an executable in PATH.
QString KMessShared::findExecutable( const QString &name )
{
return QStandardPaths::findExecutable( name );
}
/**
* @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( ";", "&#59;" )
.replace( "&", "&amp;" )
.replace( "&amp;#59;", "&#59;" ) // oh god :(
.replace( "<", "&lt;" )
.replace( ">", "&gt;" )
.replace( "'", "&#39;" )
.replace( '"', "&#34;" ); // NOTE: by not using &quot; 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( "&#34;", "\"" )
.replace( "&#39;", "'" )
.replace( "&gt;", ">" )
.replace( "&lt;", "<" )
.replace( "&#59;", "&amp;#59;" )
.replace( "&amp;", "&" )
.replace( "&#59;", ";" );
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 QUrl &url )
{
if( url.isEmpty() || ! url.isValid() )
{
kmWarning() << "Refusing to open invalid URL:" << url;
return;
}
// 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<KMessApplication *>(QApplication::instance());
QWidget *mainWindow = kmessApp->getContactListWindow()->window();
// Read which browser has to be opened
QString browserChoice( group.readEntry( "useBrowser", "KDE" ) );
#ifdef KMESSDEBUG_SHARED_OPENEXTERNAL
kmDebug() << "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" )
{
kmWarning() << "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
kmDebug() << "Custom browser selected:" << browser;
#endif
}
else if( browserChoice == "listed" )
{
browser = group.readEntry( "listedBrowser", fallbackBrowser );
#ifdef KMESSDEBUG_SHARED_OPENEXTERNAL
kmDebug() << "Listed browser selected:" << browser;
#endif
}
// If the option isn't valid use the KDE default
if( browser.isEmpty() )
{
#ifdef KMESSDEBUG_SHARED_OPENEXTERNAL
kmDebug() << "Invalid browser option, using KDE default.";
#endif
browserChoice = "KDE";
}
if( browserChoice == "KDE" )
{
#ifdef KMESSDEBUG_SHARED_OPENEXTERNAL
kmDebug() << "KDE default browser selected.";
#endif
if( ! QDesktopServices::openUrl( url ) )
{
kmWarning() << "Unable to open URL with the desktop services:" << url;
}
return;
}
// Replace the %u in the command line with the actual URL, and launch the browser.
browser.replace( "%u", KShell::quoteArg( url.toString() ), Qt::CaseInsensitive );
Q_UNUSED( mainWindow );
QProcess::startDetached( browser );
}
/**
* @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( QUrl::fromLocalFile( 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<KMessApplication *>(QApplication::instance());
QWidget *mainWindow = kmessApp->getContactListWindow()->window();
// Read which mail client has to be opened
QString mailClientChoice( group.readEntry( "useMailClient", "KDE" ) );
#ifdef KMESSDEBUG_SHARED_OPENEXTERNAL
kmDebug() << "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" )
{
kmWarning() << "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
kmDebug() << "Custom mail client selected:" << mailClient;
#endif
}
else if( mailClientChoice == "listed" )
{
mailClient = group.readEntry( "listedMailClient", fallbackMailClient );
#ifdef KMESSDEBUG_SHARED_OPENEXTERNAL
kmDebug() << "Listed mail client selected:" << mailClient;
#endif
}
// If the option isn't valid use the KDE default
if( mailClient.isEmpty() )
{
#ifdef KMESSDEBUG_SHARED_OPENEXTERNAL
kmDebug() << "Invalid mail client option, using KDE default.";
#endif
mailClientChoice = "KDE";
}
// Retrieve the mail address to open
if( mailClientChoice == "KDE" )
{
#ifdef KMESSDEBUG_SHARED_OPENEXTERNAL
kmDebug() << "KDE default mail client selected.";
#endif
QDesktopServices::openUrl( QUrl( "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 );
Q_UNUSED( mainWindow );
QProcess::startDetached( mailClient );
}
// Remove a temporary file created by downloadToLocalFile().
void KMessShared::removeDownloadedTempFile( const QString &temporaryFilePath )
{
if( ! temporaryFilePath.isEmpty() )
{
QFile::remove( temporaryFilePath );
}
}
/**
* 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
*
* <code>// 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";
}
</code>
* @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
kmDebug() << "Base file " << ( baseName + "." + extension) << "does not exist. Next sequential file is: " << ( baseName + "." + QString::number( startingNumber ) + "." + extension );
#endif
suffix = "";
return false;
}
#ifdef KMESSDEBUG_SHAREDMETHODS
kmDebug() << "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
kmDebug() << "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
kmDebug() << "Last existing file name:" << ( baseName + suffix + "." + extension );
#endif
return true;
}
/**
* Given a base directory and filename, returns the full path to the next non-existing sequentially
* numbered file. The parameter previousSequentialFile is set to the full path of the file immediately
* previous to this in the sequence.
*
* Take the following examples:
*
* @code
QString lastFile;
QString nextFile = nextSequentialFile( "./", "foo@bar.com", "xml", lastFile );
* @endcode
*
* Assume that only the base file, foo@bar.com.xml exists. Then:
*
* @code
lastFile = "./foo@bar.com.xml"
nextFile = "./foo@bar.com.1.xml"
* @endcode
*
* Assume that the files foo@bar.com.xml, foo@bar.com.1.xml and foo@bar.com.2.xml exist. Then:
*
* @code
previousFile = "./foo@bar.com.2.xml"
nextFile = "./foo@bar.com.3.xml"
* @endcode
*
* etc.
*
* @param baseDirPath The full path to the base directory.
* @param baseFileName The base file name, minus the extension or numbering.
* @param extension The extension of the file.
* @param lastFile [out] Set to the full path of the last existing file in the sequence, or an empty string if none exists.
* @return The full path to the next unused sequential file.
*/
QString KMessShared::nextSequentialFile( const QString &baseDirPath, const QString &baseFileName, const QString &extension, QString &lastFile )
{
QDir baseDir( baseDirPath );
baseDir.setFilter( QDir::Files );
baseDir.setNameFilters( QStringList() << ( baseFileName + "*." + extension ) );
// Get the list of all .1, .2, etc log files.
// See KMessShared::compareFileNames()
QStringList numberedFiles( baseDir.entryList() );
std::sort( numberedFiles.begin(), numberedFiles.end(), KMessShared::compareFileNames );
// There are no matching files
if( numberedFiles.size() == 0 )
{
lastFile = QString();
return baseDir.absoluteFilePath( baseFileName + "." + extension );
}
QString nextFile;
lastFile = baseDir.absoluteFilePath( numberedFiles.last() );
// one or more numbered files exist. grab the number of the most recent, increment it.
// that becomes the number of the next numbered file.
int pos = lastFile.indexOf( baseFileName ) + baseFileName.length() + 1;
QString numberPart( lastFile.mid( pos, lastFile.lastIndexOf( extension ) - pos - 1 ) );
bool ok = false;
int number = numberPart.toInt( &ok );
// The file is in form: baseFileName.extension
if( numberPart == extension || ! ok )
{
return baseDir.absoluteFilePath( baseFileName + ".1." + extension );
}
// The file is in form: baseFileName.number.extension
else
{
return baseDir.absoluteFilePath( baseFileName + "." + QString::number( number + 1 ) + "." + extension );
}
}
// 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.unicode() <= 0x007f )
{
more = 1;
}
else if( character.unicode() <= 0x07FF )
{
more = 2;
}
else if( character.unicode() <= 0xd7ff )
{
more = 3;
}
else if( character.unicode() <= 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;
}