kpimidentities
signature.cpp
00001 /* 00002 Copyright (c) 2002-2004 Marc Mutz <mutz@kde.org> 00003 Copyright (c) 2007 Tom Albers <tomalbers@kde.nl> 00004 Copyright (c) 2009 Thomas McGuire <mcguire@kde.org> 00005 00006 This library is free software; you can redistribute it and/or modify it 00007 under the terms of the GNU Library General Public License as published by 00008 the Free Software Foundation; either version 2 of the License, or (at your 00009 option) any later version. 00010 00011 This library is distributed in the hope that it will be useful, but WITHOUT 00012 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 00013 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 00014 License for more details. 00015 00016 You should have received a copy of the GNU Library General Public License 00017 along with this library; see the file COPYING.LIB. If not, write to the 00018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 00019 02110-1301, USA. 00020 */ 00021 00022 #include "signature.h" 00023 00024 #include <kdebug.h> 00025 #include <klocale.h> 00026 #include <kmessagebox.h> 00027 #include <kconfiggroup.h> 00028 #include <kurl.h> 00029 #include <kprocess.h> 00030 #include <KRichTextEdit> 00031 #include <kpimutils/kfileio.h> 00032 00033 #include <QFileInfo> 00034 #include <QSharedPointer> 00035 #include <QImage> 00036 00037 #include <assert.h> 00038 #include <QtCore/QDir> 00039 #include <kpimtextedit/textedit.h> 00040 00041 using namespace KPIMIdentities; 00042 00043 class SignaturePrivate 00044 { 00045 public: 00046 struct EmbeddedImage 00047 { 00048 QImage image; 00049 QString name; 00050 }; 00051 typedef QSharedPointer<EmbeddedImage> EmbeddedImagePtr; 00052 00055 QList<EmbeddedImagePtr> embeddedImages; 00056 00058 QString saveLocation; 00059 }; 00060 00061 QDataStream &operator<< ( QDataStream &stream, const SignaturePrivate::EmbeddedImagePtr &img ) 00062 { 00063 return stream << img->image << img->name; 00064 } 00065 00066 QDataStream &operator>> ( QDataStream &stream, SignaturePrivate::EmbeddedImagePtr &img ) 00067 { 00068 return stream >> img->image >> img->name; 00069 } 00070 00071 // TODO: KDE5: BIC: Add a real d-pointer. 00072 // This QHash is just a workaround around BIC issues, for more info see 00073 // http://techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C++ 00074 typedef QHash<const Signature*,SignaturePrivate*> SigPrivateHash; 00075 Q_GLOBAL_STATIC(SigPrivateHash, d_func) 00076 00077 static SignaturePrivate* d( const Signature *sig ) 00078 { 00079 SignaturePrivate *ret = d_func()->value( sig, 0 ); 00080 if ( !ret ) { 00081 ret = new SignaturePrivate; 00082 d_func()->insert( sig, ret ); 00083 } 00084 return ret; 00085 } 00086 00087 static void delete_d( const Signature* sig ) 00088 { 00089 SignaturePrivate *ret = d_func()->value( sig, 0 ); 00090 delete ret; 00091 d_func()->remove( sig ); 00092 } 00093 00094 Signature::Signature() 00095 : mType( Disabled ), 00096 mInlinedHtml( false ) 00097 {} 00098 00099 Signature::Signature( const QString &text ) 00100 : mText( text ), 00101 mType( Inlined ), 00102 mInlinedHtml( false ) 00103 {} 00104 00105 Signature::Signature( const QString &url, bool isExecutable ) 00106 : mUrl( url ), 00107 mType( isExecutable ? FromCommand : FromFile ), 00108 mInlinedHtml( false ) 00109 {} 00110 00111 void Signature::assignFrom ( const KPIMIdentities::Signature &that ) 00112 { 00113 mUrl = that.mUrl; 00114 mInlinedHtml = that.mInlinedHtml; 00115 mText = that.mText; 00116 mType = that.mType; 00117 d( this )->saveLocation = d( &that )->saveLocation; 00118 d( this )->embeddedImages = d( &that )->embeddedImages; 00119 } 00120 00121 Signature::Signature ( const Signature &that ) 00122 { 00123 assignFrom( that ); 00124 } 00125 00126 Signature& Signature::operator= ( const KPIMIdentities::Signature & that ) 00127 { 00128 if ( this == &that ) 00129 return *this; 00130 00131 assignFrom( that ); 00132 return *this; 00133 } 00134 00135 Signature::~Signature() 00136 { 00137 delete_d( this ); 00138 } 00139 00140 QString Signature::rawText( bool *ok ) const 00141 { 00142 switch ( mType ) { 00143 case Disabled: 00144 if ( ok ) { 00145 *ok = true; 00146 } 00147 return QString(); 00148 case Inlined: 00149 if ( ok ) { 00150 *ok = true; 00151 } 00152 return mText; 00153 case FromFile: 00154 return textFromFile( ok ); 00155 case FromCommand: 00156 return textFromCommand( ok ); 00157 }; 00158 kFatal(5325) << "Signature::type() returned unknown value!"; 00159 return QString(); // make compiler happy 00160 } 00161 00162 QString Signature::textFromCommand( bool *ok ) const 00163 { 00164 assert( mType == FromCommand ); 00165 00166 // handle pathological cases: 00167 if ( mUrl.isEmpty() ) { 00168 if ( ok ) { 00169 *ok = true; 00170 } 00171 return QString(); 00172 } 00173 00174 // create a shell process: 00175 KProcess proc; 00176 proc.setOutputChannelMode( KProcess::SeparateChannels ); 00177 proc.setShellCommand( mUrl ); 00178 int rc = proc.execute(); 00179 00180 // handle errors, if any: 00181 if ( rc != 0 ) { 00182 if ( ok ) { 00183 *ok = false; 00184 } 00185 QString wmsg = i18n( "<qt>Failed to execute signature script<p><b>%1</b>:</p>" 00186 "<p>%2</p></qt>", mUrl, QString( proc.readAllStandardError() ) ); 00187 KMessageBox::error( 0, wmsg ); 00188 return QString(); 00189 } 00190 00191 // no errors: 00192 if ( ok ) { 00193 *ok = true; 00194 } 00195 00196 // get output: 00197 QByteArray output = proc.readAllStandardOutput(); 00198 00199 // TODO: hmm, should we allow other encodings, too? 00200 return QString::fromLocal8Bit( output.data(), output.size() ); 00201 } 00202 00203 QString Signature::textFromFile( bool *ok ) const 00204 { 00205 assert( mType == FromFile ); 00206 00207 // TODO: Use KIO::NetAccess to download non-local files! 00208 if ( !KUrl( mUrl ).isLocalFile() && 00209 !( QFileInfo( mUrl ).isRelative() && 00210 QFileInfo( mUrl ).exists() ) ) { 00211 kDebug(5325) << "Signature::textFromFile:" 00212 << "non-local URLs are unsupported"; 00213 if ( ok ) { 00214 *ok = false; 00215 } 00216 return QString(); 00217 } 00218 00219 if ( ok ) { 00220 *ok = true; 00221 } 00222 00223 // TODO: hmm, should we allow other encodings, too? 00224 const QByteArray ba = KPIMUtils::kFileToByteArray( mUrl, false ); 00225 return QString::fromLocal8Bit( ba.data(), ba.size() ); 00226 } 00227 00228 QString Signature::withSeparator( bool *ok ) const 00229 { 00230 QString signature = rawText( ok ); 00231 if ( ok && (*ok) == false ) 00232 return QString(); 00233 00234 if ( signature.isEmpty() ) { 00235 return signature; // don't add a separator in this case 00236 } 00237 00238 const bool htmlSig = ( isInlinedHtml() && mType == Inlined ); 00239 QString newline = htmlSig ? "<br>" : "\n"; 00240 if ( htmlSig && signature.startsWith( QLatin1String( "<p" ) ) ) { 00241 newline.clear(); 00242 } 00243 00244 if ( signature.startsWith( QString::fromLatin1( "-- " ) + newline ) 00245 || ( signature.indexOf( newline + QString::fromLatin1( "-- " ) + 00246 newline ) != -1 ) ) { 00247 // already have signature separator at start of sig or inside sig: 00248 return signature; 00249 } else { 00250 // need to prepend one: 00251 return QString::fromLatin1( "-- " ) + newline + signature; 00252 } 00253 } 00254 00255 void Signature::setUrl( const QString &url, bool isExecutable ) 00256 { 00257 mUrl = url; 00258 mType = isExecutable ? FromCommand : FromFile; 00259 } 00260 00261 void Signature::setInlinedHtml( bool isHtml ) 00262 { 00263 mInlinedHtml = isHtml; 00264 } 00265 00266 bool Signature::isInlinedHtml() const 00267 { 00268 return mInlinedHtml; 00269 } 00270 00271 // config keys and values: 00272 static const char sigTypeKey[] = "Signature Type"; 00273 static const char sigTypeInlineValue[] = "inline"; 00274 static const char sigTypeFileValue[] = "file"; 00275 static const char sigTypeCommandValue[] = "command"; 00276 static const char sigTypeDisabledValue[] = "disabled"; 00277 static const char sigTextKey[] = "Inline Signature"; 00278 static const char sigFileKey[] = "Signature File"; 00279 static const char sigCommandKey[] = "Signature Command"; 00280 static const char sigTypeInlinedHtmlKey[] = "Inlined Html"; 00281 static const char sigImageLocation[] = "Image Location"; 00282 00283 // Returns the names of all images in the HTML code 00284 static QStringList findImageNames( const QString &htmlCode ) 00285 { 00286 QStringList ret; 00287 00288 // To complicated for us, so cheat and let a text edit do the hard work 00289 KPIMTextEdit::TextEdit edit; 00290 edit.setHtml( htmlCode ); 00291 foreach( const KPIMTextEdit::ImageWithNamePtr &image, edit.imagesWithName() ) { 00292 ret << image->name; 00293 } 00294 return ret; 00295 } 00296 00297 void Signature::cleanupImages() const 00298 { 00299 // Remove any images from the internal structure that are no longer there 00300 if ( isInlinedHtml() ) { 00301 foreach( const SignaturePrivate::EmbeddedImagePtr &imageInList, d( this )->embeddedImages ) { 00302 bool found = false; 00303 foreach( const QString &imageInHtml, findImageNames( mText ) ) { 00304 if ( imageInHtml == imageInList->name ) { 00305 found = true; 00306 break; 00307 } 00308 } 00309 if ( !found ) 00310 d( this )->embeddedImages.removeAll( imageInList ); 00311 } 00312 } 00313 00314 // Delete all the old image files 00315 if ( !d( this )->saveLocation.isEmpty() ) { 00316 QDir dir( d( this )->saveLocation ); 00317 foreach( const QString &fileName, dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ) ) { 00318 if ( fileName.toLower().endsWith( QLatin1String( ".png" ) ) ) { 00319 kDebug() << "Deleting old image" << dir.path() + fileName; 00320 dir.remove( fileName ); 00321 } 00322 } 00323 } 00324 } 00325 00326 void Signature::saveImages() const 00327 { 00328 if ( isInlinedHtml() && !d( this )->saveLocation.isEmpty() ) { 00329 foreach( const SignaturePrivate::EmbeddedImagePtr &image, d( this )->embeddedImages ) { 00330 QString location = d( this )->saveLocation + '/' + image->name; 00331 if ( !image->image.save( location, "PNG" ) ) { 00332 kWarning() << "Failed to save image" << location; 00333 } 00334 } 00335 } 00336 } 00337 00338 void Signature::readConfig( const KConfigGroup &config ) 00339 { 00340 QString sigType = config.readEntry( sigTypeKey ); 00341 if ( sigType == sigTypeInlineValue ) { 00342 mType = Inlined; 00343 mInlinedHtml = config.readEntry( sigTypeInlinedHtmlKey, false ); 00344 } else if ( sigType == sigTypeFileValue ) { 00345 mType = FromFile; 00346 mUrl = config.readPathEntry( sigFileKey, QString() ); 00347 } else if ( sigType == sigTypeCommandValue ) { 00348 mType = FromCommand; 00349 mUrl = config.readPathEntry( sigCommandKey, QString() ); 00350 } else { 00351 mType = Disabled; 00352 } 00353 mText = config.readEntry( sigTextKey ); 00354 d( this )->saveLocation = config.readEntry( sigImageLocation ); 00355 00356 if ( isInlinedHtml() && !d( this )->saveLocation.isEmpty() ) { 00357 QDir dir( d( this )->saveLocation ); 00358 foreach( const QString &fileName, dir.entryList( QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks ) ) { 00359 if ( fileName.toLower().endsWith( QLatin1String( ".png" ) ) ) { 00360 QImage image; 00361 if ( image.load( dir.path() + '/' + fileName ) ) { 00362 addImage( image, fileName ); 00363 } 00364 else { 00365 kWarning() << "Unable to load image" << dir.path() + '/' + fileName; 00366 } 00367 } 00368 } 00369 } 00370 } 00371 00372 void Signature::writeConfig( KConfigGroup &config ) const 00373 { 00374 switch ( mType ) { 00375 case Inlined: 00376 config.writeEntry( sigTypeKey, sigTypeInlineValue ); 00377 config.writeEntry( sigTypeInlinedHtmlKey, mInlinedHtml ); 00378 break; 00379 case FromFile: 00380 config.writeEntry( sigTypeKey, sigTypeFileValue ); 00381 config.writePathEntry( sigFileKey, mUrl ); 00382 break; 00383 case FromCommand: 00384 config.writeEntry( sigTypeKey, sigTypeCommandValue ); 00385 config.writePathEntry( sigCommandKey, mUrl ); 00386 break; 00387 case Disabled: 00388 config.writeEntry( sigTypeKey, sigTypeDisabledValue ); 00389 default: 00390 break; 00391 } 00392 config.writeEntry( sigTextKey, mText ); 00393 config.writeEntry( sigImageLocation, d( this )->saveLocation ); 00394 00395 cleanupImages(); 00396 saveImages(); 00397 } 00398 00399 static bool isCursorAtEndOfLine( const QTextCursor &cursor ) 00400 { 00401 QTextCursor testCursor = cursor; 00402 testCursor.movePosition( QTextCursor::EndOfLine, QTextCursor::KeepAnchor ); 00403 return !testCursor.hasSelection(); 00404 } 00405 00406 static void insertSignatureHelper( const QString &signature, 00407 KRichTextEdit *textEdit, 00408 Signature::Placement placement, 00409 bool isHtml, 00410 bool addNewlines ) 00411 { 00412 if ( !signature.isEmpty() ) { 00413 00414 // Save the modified state of the document, as inserting a signature 00415 // shouldn't change this. Restore it at the end of this function. 00416 bool isModified = textEdit->document()->isModified(); 00417 00418 // Move to the desired position, where the signature should be inserted 00419 QTextCursor cursor = textEdit->textCursor(); 00420 QTextCursor oldCursor = cursor; 00421 cursor.beginEditBlock(); 00422 00423 if ( placement == Signature::End ) 00424 cursor.movePosition( QTextCursor::End ); 00425 else if ( placement == Signature::Start ) 00426 cursor.movePosition( QTextCursor::Start ); 00427 else if ( placement == Signature::AtCursor ) 00428 cursor.movePosition( QTextCursor::StartOfLine ); 00429 textEdit->setTextCursor( cursor ); 00430 00431 00432 QString lineSep; 00433 if ( addNewlines ) { 00434 if ( isHtml ) 00435 lineSep = QLatin1String( "<br>" ); 00436 else 00437 lineSep = QLatin1Char( '\n' ); 00438 } 00439 00440 // Insert the signature and newlines depending on where it was inserted. 00441 bool hackForCursorsAtEnd = false; 00442 int oldCursorPos = -1; 00443 if ( placement == Signature::End ) { 00444 00445 if ( oldCursor.position() == textEdit->toPlainText().length() ) { 00446 hackForCursorsAtEnd = true; 00447 oldCursorPos = oldCursor.position(); 00448 } 00449 00450 if ( isHtml ) { 00451 textEdit->insertHtml( lineSep + signature ); 00452 } else { 00453 textEdit->insertPlainText( lineSep + signature ); 00454 } 00455 } else if ( placement == Signature::Start || placement == Signature::AtCursor ) { 00456 if ( isHtml ) { 00457 if ( isCursorAtEndOfLine( cursor ) ) 00458 textEdit->insertHtml( signature ); 00459 else 00460 textEdit->insertHtml( signature + lineSep ); 00461 } else { 00462 if ( isCursorAtEndOfLine( cursor ) ) 00463 textEdit->insertPlainText( signature ); 00464 else 00465 textEdit->insertPlainText( signature + lineSep ); 00466 } 00467 } 00468 00469 cursor.endEditBlock(); 00470 00471 // There is one special case when re-setting the old cursor: The cursor 00472 // was at the end. In this case, QTextEdit has no way to know 00473 // if the signature was added before or after the cursor, and just decides 00474 // that it was added before (and the cursor moves to the end, but it should 00475 // not when appending a signature). See bug 167961 00476 if ( hackForCursorsAtEnd ) 00477 oldCursor.setPosition( oldCursorPos ); 00478 00479 textEdit->setTextCursor( oldCursor ); 00480 textEdit->ensureCursorVisible(); 00481 00482 textEdit->document()->setModified( isModified ); 00483 00484 if ( isHtml ) { 00485 textEdit->enableRichTextMode(); 00486 } 00487 } 00488 } 00489 00490 void Signature::insertIntoTextEdit( KRichTextEdit *textEdit, 00491 Placement placement, bool addSeparator ) 00492 { 00493 QString signature; 00494 if ( addSeparator ) 00495 signature = withSeparator(); 00496 else 00497 signature = rawText(); 00498 00499 insertSignatureHelper( signature, textEdit, placement, 00500 ( isInlinedHtml() && 00501 type() == KPIMIdentities::Signature::Inlined ), 00502 true ); 00503 } 00504 00505 void Signature::insertIntoTextEdit( Placement placement, AddedText addedText, 00506 KPIMTextEdit::TextEdit *textEdit ) const 00507 { 00508 QString signature; 00509 if ( addedText & AddSeparator ) 00510 signature = withSeparator(); 00511 else 00512 signature = rawText(); 00513 00514 insertSignatureHelper( signature, textEdit, placement, 00515 ( isInlinedHtml() && 00516 type() == KPIMIdentities::Signature::Inlined ), 00517 ( addedText & AddNewLines ) ); 00518 00519 // We added the text of the signature above, now it is time to add the images as well. 00520 if ( isInlinedHtml() ) { 00521 foreach( const SignaturePrivate::EmbeddedImagePtr &image, d( this )->embeddedImages ) { 00522 textEdit->loadImage( image->image, image->name, image->name ); 00523 } 00524 } 00525 } 00526 00527 void Signature::insertPlainSignatureIntoTextEdit( const QString &signature, KRichTextEdit *textEdit, 00528 Signature::Placement placement, bool isHtml ) 00529 { 00530 insertSignatureHelper( signature, textEdit, placement, isHtml, true ); 00531 } 00532 00533 // --------------------- Operators -------------------// 00534 00535 QDataStream &KPIMIdentities::operator<< 00536 ( QDataStream &stream, const KPIMIdentities::Signature &sig ) 00537 { 00538 return stream << static_cast<quint8>( sig.mType ) << sig.mUrl << sig.mText 00539 << d( &sig )->saveLocation << d( &sig )->embeddedImages; 00540 } 00541 00542 QDataStream &KPIMIdentities::operator>> 00543 ( QDataStream &stream, KPIMIdentities::Signature &sig ) 00544 { 00545 quint8 s; 00546 stream >> s >> sig.mUrl >> sig.mText >> d( &sig )->saveLocation >> d( &sig )->embeddedImages; 00547 sig.mType = static_cast<Signature::Type>( s ); 00548 return stream; 00549 } 00550 00551 bool Signature::operator== ( const Signature &other ) const 00552 { 00553 if ( mType != other.mType ) { 00554 return false; 00555 } 00556 00557 if ( mType == Inlined && mInlinedHtml ) { 00558 if ( d( this )->saveLocation != d( &other )->saveLocation ) 00559 return false; 00560 if ( d( this )->embeddedImages != d( &other )->embeddedImages ) 00561 return false; 00562 } 00563 00564 switch ( mType ) { 00565 case Inlined: 00566 return mText == other.mText; 00567 case FromFile: 00568 case FromCommand: 00569 return mUrl == other.mUrl; 00570 default: 00571 case Disabled: 00572 return true; 00573 } 00574 } 00575 00576 QString Signature::toPlainText() const 00577 { 00578 QString sigText = rawText(); 00579 if ( isInlinedHtml() && type() == Inlined ) { 00580 // Use a QTextDocument as a helper, it does all the work for us and 00581 // strips all HTML tags. 00582 QTextDocument helper; 00583 QTextCursor helperCursor( &helper ); 00584 helperCursor.insertHtml( sigText ); 00585 sigText = helper.toPlainText(); 00586 } 00587 return sigText; 00588 } 00589 00590 void Signature::addImage ( const QImage& imageData, const QString& imageName ) 00591 { 00592 Q_ASSERT( !( d( this )->saveLocation.isEmpty() ) ); 00593 SignaturePrivate::EmbeddedImagePtr image( new SignaturePrivate::EmbeddedImage() ); 00594 image->image = imageData; 00595 image->name = imageName; 00596 d( this )->embeddedImages.append( image ); 00597 } 00598 00599 void Signature::setImageLocation ( const QString& path ) 00600 { 00601 d( this )->saveLocation = path; 00602 } 00603 00604 // --------------- Getters -----------------------// 00605 00606 QString Signature::text() const 00607 { 00608 return mText; 00609 } 00610 00611 QString Signature::url() const 00612 { 00613 return mUrl; 00614 } 00615 00616 Signature::Type Signature::type() const 00617 { 00618 return mType; 00619 } 00620 00621 // --------------- Setters -----------------------// 00622 00623 void Signature::setText( const QString &text ) 00624 { 00625 mText = text; 00626 mType = Inlined; 00627 } 00628 00629 void Signature::setType( Type type ) 00630 { 00631 mType = type; 00632 }