2 kopetemessage.cpp - Base class for Kopete messages
4 Copyright (c) 2002-2003 by Martijn Klingens <klingens@kde.org>
5 Copyright (c) 2002-2006 by Olivier Goffart <ogoffart@kde.org>
6 Copyright (c) 2006-2007 by Charles Connell <charles@connells.org>
7 Copyright (c) 2007 by Michaƫl Larouche <larouche@kde.org>
9 Kopete (c) 2002-2005 by the Kopete developers <kopete-devel@kde.org>
11 *************************************************************************
13 * This library is free software; you can redistribute it and/or *
14 * modify it under the terms of the GNU Lesser General Public *
15 * License as published by the Free Software Foundation; either *
16 * version 2 of the License, or (at your option) any later version. *
18 *************************************************************************
23 #include <QtCore/QDateTime>
24 #include <QtCore/QLatin1String>
25 #include <QtCore/QPointer>
26 #include <QtCore/QRegExp>
27 #include <QtCore/QTextCodec>
28 #include <QtGui/QTextDocument>
29 #include <QtGui/QColor>
32 #include <kstringhandler.h>
33 #include <kemoticons.h>
35 #include "kopetemessage.h"
36 #include "kopetemetacontact.h"
37 #include "kopeteprotocol.h"
38 #include "kopetechatsession.h"
39 #include "kopetecontact.h"
40 #include "kopeteemoticons.h"
46 class Message::Private
51 : direction(Internal
), format(Qt::PlainText
), type(TypeNormal
), importance(Normal
), backgroundOverride(false),
52 foregroundOverride(false), richTextOverride(false), isRightToLeft(false), timeStamp( QDateTime::currentDateTime() ),
53 body(new QTextDocument
), escapedBodyDirty(true)
55 Private (const Private
&other
);
58 QPointer
<Contact
> from
;
60 QPointer
<ChatSession
> manager
;
62 MessageDirection direction
;
63 Qt::TextFormat format
;
65 QString requestedPlugin
;
66 MessageImportance importance
;
67 bool backgroundOverride
;
68 bool foregroundOverride
;
69 bool richTextOverride
;
75 QColor foregroundColor
;
76 QColor backgroundColor
;
80 mutable QString escapedBody
;
81 mutable bool escapedBodyDirty
;
84 Message::Private::Private (const Message::Private
&other
)
89 manager
= other
.manager
;
91 direction
= other
.direction
;
92 format
= other
.format
;
94 requestedPlugin
= other
.requestedPlugin
;
95 importance
= other
.importance
;
96 backgroundOverride
= other
.backgroundOverride
;
97 foregroundOverride
= other
.foregroundOverride
;
98 richTextOverride
= other
.richTextOverride
;
99 isRightToLeft
= other
.isRightToLeft
;
100 timeStamp
= other
.timeStamp
;
102 classes
= other
.classes
;
104 foregroundColor
= other
.foregroundColor
;
105 backgroundColor
= other
.backgroundColor
;
106 subject
= other
.subject
;
108 body
= other
.body
->clone();
109 escapedBody
= other
.escapedBody
;
110 escapedBodyDirty
= other
.escapedBodyDirty
;
113 Message::Private::~Private ()
123 Message::Message( const Contact
*fromKC
, const Contact
*toKC
)
126 d
->from
= const_cast<Contact
*>(fromKC
);
127 QList
<Contact
*> contacts
;
128 contacts
<< const_cast<Contact
*>(toKC
);
133 Message::Message( const Contact
*fromKC
, const QList
<Contact
*> &toKC
)
136 d
->from
= const_cast<Contact
*>(fromKC
);
140 Message::Message( const Message
&other
)
145 Message
& Message::operator=( const Message
&other
)
155 void Message::setBackgroundOverride( bool enabled
)
157 d
->backgroundOverride
= enabled
;
160 void Message::setForegroundOverride( bool enabled
)
162 d
->foregroundOverride
= enabled
;
165 void Message::setRichTextOverride( bool enabled
)
167 d
->richTextOverride
= enabled
;
170 void Message::setForegroundColor( const QColor
&color
)
172 d
->foregroundColor
=color
;
175 void Message::setBackgroundColor( const QColor
&color
)
177 d
->backgroundColor
=color
;
180 void Message::setFont( const QFont
&font
)
185 void Message::setPlainBody (const QString
&body
)
187 doSetBody (body
, Qt::PlainText
);
190 void Message::setHtmlBody (const QString
&body
)
192 doSetBody (body
, Qt::RichText
);
195 void Message::doSetBody (const QString
&body
, Qt::TextFormat f
)
197 if (f
== Qt::PlainText
)
198 d
->body
->setPlainText(body
);
200 d
->body
->setHtml(body
);
202 d
->isRightToLeft
= d
->body
->toPlainText().isRightToLeft();
203 d
->escapedBodyDirty
= true;
206 void Message::setBody (const QTextDocument
*_body
)
208 doSetBody (_body
, Qt::RichText
);
211 void Message::doSetBody (const QTextDocument
*body
, Qt::TextFormat f
)
214 d
->body
= body
->clone(); // delete the old body and replace it with a *copy* of the new one
216 d
->isRightToLeft
= d
->body
->toPlainText().isRightToLeft();
217 d
->escapedBodyDirty
= true;
220 void Message::setImportance(Message::MessageImportance i
)
225 QString
Message::unescape( const QString
&xml
)
229 // Remove linebreak and multiple spaces. First return nbsp's to normal spaces :)
230 data
= data
.simplified();
233 while ( ( pos
= data
.indexOf( '<' ) ) != -1 )
235 int endPos
= data
.indexOf( '>', pos
+ 1 );
237 break; // No more complete elements left
239 // Take the part between < and >, and extract the element name from that
240 int matchWidth
= endPos
- pos
+ 1;
241 QString match
= data
.mid( pos
+ 1, matchWidth
- 2 ).simplified();
242 int elemEndPos
= match
.indexOf( ' ' );
243 QString elem
= ( elemEndPos
== -1 ? match
.toLower() : match
.left( elemEndPos
).toLower() );
244 if ( elem
== QLatin1String( "img" ) )
246 // Replace smileys with their original text'
247 const QString attrTitle
= QLatin1String( "title=\"" );
248 int titlePos
= match
.indexOf( attrTitle
, elemEndPos
);
249 int titleEndPos
= match
.indexOf( '"', titlePos
+ attrTitle
.length() );
250 if( titlePos
== -1 || titleEndPos
== -1 )
252 // Not a smiley but a normal <img>
253 // Don't update pos, we restart at this position :)
254 data
.remove( pos
, matchWidth
);
258 QString orig
= match
.mid( titlePos
+ attrTitle
.length(),
259 titleEndPos
- titlePos
- attrTitle
.length() );
260 data
.replace( pos
, matchWidth
, orig
);
261 pos
+= orig
.length();
264 else if ( elem
== QLatin1String( "/p" ) || elem
== QLatin1String( "/div" ) ||
265 elem
== QLatin1String( "br" ) )
267 // Replace paragraph, div and line breaks with a newline
268 data
.replace( pos
, matchWidth
, '\n' );
273 // Remove all other elements entirely
274 // Don't update pos, we restart at this position :)
275 data
.remove( pos
, matchWidth
);
279 // Replace stuff starting with '&'
280 data
.replace( QLatin1String( ">" ), QLatin1String( ">" ) );
281 data
.replace( QLatin1String( "<" ), QLatin1String( "<" ) );
282 data
.replace( QLatin1String( """ ), QLatin1String( "\"" ) );
283 data
.replace( QLatin1String( " " ), QLatin1String( " " ) );
284 data
.replace( QLatin1String( "&" ), QLatin1String( "&" ) );
285 data
.replace( QLatin1String( " " ), QLatin1String( " " ) ); //this one is used in jabber: note, we should escape all &#xx;
290 QString
Message::escape( const QString
&text
)
292 QString html
= Qt::escape( text
);
293 //Replace carriage returns inside the text
294 html
.replace( QLatin1String( "\n" ), QLatin1String( "<br />" ) );
295 //Replace a tab with 4 spaces
296 html
.replace( QLatin1String( "\t" ), QLatin1String( " " ) );
298 //Replace multiple spaces with
299 //do not replace every space so we break the linebreak
300 html
.replace( QRegExp( QLatin1String( "\\s\\s" ) ), QLatin1String( " " ) );
305 QString
Message::plainBody() const
307 return d
->body
->toPlainText();
310 QString
Message::escapedBody() const
312 // kDebug(14010) << escapedBody() << " " << d->richTextOverride;
314 // the escaped body is cached because QRegExp is very expensive, so it shouldn't be used any more than nescessary
315 if (!d
->escapedBodyDirty
)
316 return d
->escapedBody
;
319 if ( d
->format
== Qt::PlainText
)
320 html
= Qt::convertFromPlainText( d
->body
->toPlainText(), Qt::WhiteSpaceNormal
);
322 html
= d
->body
->toHtml();
324 // all this regex business is to take off the outer HTML document provided by QTextDocument
326 QRegExp
badStuff ("<![^<>]*>|<head[^<>]*>.*</head[^<>]*>|</?html[^<>]*>|</?body[^<>]*>");
327 html
= html
.remove (badStuff
);
328 // remove newlines that may be present, since they end up being displayed in the chat window. real newlines are represented with <br>, so we know \n's are meaningless
329 html
= html
.remove ("\n");
330 d
->escapedBody
= html
;
331 d
->escapedBodyDirty
= false;
336 QString
Message::parsedBody() const
338 //kDebug(14000) << "messageformat: " << d->format;
340 return Kopete::Emoticons::self()->theme().parseEmoticons(parseLinks(escapedBody(), Qt::RichText
));
343 static QString
makeRegExp( const char *pattern
)
345 const QString urlChar
= QLatin1String( "\\+\\-\\w\\./#@&;:=\\?~%_,\\!\\$\\*\\(\\)" );
346 const QString boundaryStart
= QString( "(^|[^%1])(" ).arg( urlChar
);
347 const QString boundaryEnd
= QString( ")([^%1]|$)" ).arg( urlChar
);
349 return boundaryStart
+ QLatin1String(pattern
) + boundaryEnd
;
352 QString
Message::parseLinks( const QString
&message
, Qt::TextFormat format
)
354 if ( format
& Qt::RichText
)
356 // < in HTML *always* means start-of-tag
357 QStringList entries
= message
.split( QChar('<'), QString::KeepEmptyParts
);
359 QStringList::Iterator it
= entries
.begin();
361 // first one is different: it doesn't start with an HTML tag.
362 if ( it
!= entries
.end() )
364 *it
= parseLinks( *it
, Qt::PlainText
);
368 for ( ; it
!= entries
.end(); ++it
)
371 // > in HTML means start-of-tag if and only if it's the first one after a <
372 int tagclose
= curr
.indexOf( QChar('>') );
373 // no >: the HTML is broken, but we can cope
374 if ( tagclose
== -1 )
376 QString tag
= curr
.left( tagclose
+ 1 );
377 QString body
= curr
.mid( tagclose
+ 1 );
378 *it
= tag
+ parseLinks( body
, Qt::PlainText
);
380 return entries
.join(QLatin1String("<"));
383 QString result
= message
;
385 // common subpatterns - may not contain matching parens!
386 const QString name
= QLatin1String( "[\\w\\+\\-=_\\.]+" );
387 const QString userAndPassword
= QString( "(?:%1(?::%1)?\\@)" ).arg( name
);
388 const QString urlChar
= QLatin1String( "\\+\\-\\w\\./#@&;:=\\?~%_,\\!\\$\\*\\(\\)" );
389 const QString urlSection
= QString( "[%1]+" ).arg( urlChar
);
390 const QString domain
= QLatin1String( "[\\-\\w_]+(?:\\.[\\-\\w_]+)+" );
392 //Replace http/https/ftp links:
393 // Replace (stuff)://[user:password@](linkstuff) with a link
395 QRegExp( makeRegExp("\\w+://%1?\\w%2").arg( userAndPassword
, urlSection
) ),
396 QLatin1String("\\1<a href=\"\\2\" title=\"\\2\">\\2</a>\\3" ) );
398 // Replace www.X.Y(linkstuff) with a http: link
400 QRegExp( makeRegExp("%1?www\\.%2%3").arg( userAndPassword
, domain
, urlSection
) ),
401 QLatin1String("\\1<a href=\"http://\\2\" title=\"http://\\2\">\\2</a>\\3" ) );
403 //Replace Email Links
404 // Replace user@domain with a mailto: link
406 QRegExp( makeRegExp("%1@%2").arg( name
, domain
) ),
407 QLatin1String("\\1<a href=\"mailto:\\2\" title=\"mailto:\\2\">\\2</a>\\3") );
409 //Workaround for Bug 85061: Highlighted URLs adds a ' ' after the URL itself
410 // the trailing is included in the url.
411 result
.replace( QRegExp( QLatin1String("(<a href=\"[^\"]+)( )(\")") ) , QLatin1String("\\1\\3") );
418 QDateTime
Message::timestamp() const
423 void Message::setTimestamp(const QDateTime
×tamp
)
425 d
->timeStamp
= timestamp
;
428 const Contact
*Message::from() const
433 QList
<Contact
*> Message::to() const
438 Message::MessageType
Message::type() const
443 void Message::setType(MessageType type
)
448 QString
Message::requestedPlugin() const
450 return d
->requestedPlugin
;
453 void Message::setRequestedPlugin(const QString
&requestedPlugin
)
455 d
->requestedPlugin
= requestedPlugin
;
458 QColor
Message::foregroundColor() const
460 return d
->foregroundColor
;
463 QColor
Message::backgroundColor() const
465 return d
->backgroundColor
;
468 bool Message::isRightToLeft() const
470 return d
->isRightToLeft
;
473 QFont
Message::font() const
478 QString
Message::subject() const
483 void Message::setSubject(const QString
&subject
)
485 d
->subject
= subject
;
488 const QTextDocument
*Message::body() const
493 Qt::TextFormat
Message::format() const
498 Message::MessageDirection
Message::direction() const
503 void Message::setDirection(MessageDirection direction
)
505 d
->direction
= direction
;
508 Message::MessageImportance
Message::importance() const
510 return d
->importance
;
513 ChatSession
*Message::manager() const
518 void Message::setManager(ChatSession
*kmm
)
523 QString
Message::getHtmlStyleAttribute() const
525 QString styleAttribute
;
527 styleAttribute
= QString::fromUtf8("style=\"");
529 // Affect foreground(color) and background color to message.
530 if( !d
->foregroundOverride
&& d
->foregroundColor
.isValid() )
532 styleAttribute
+= QString::fromUtf8("color: %1; ").arg(d
->foregroundColor
.name());
534 if( !d
->backgroundOverride
&& d
->backgroundColor
.isValid() )
536 styleAttribute
+= QString::fromUtf8("background-color: %1; ").arg(d
->backgroundColor
.name());
539 // Affect font parameters.
540 if( !d
->richTextOverride
&& d
->font
!=QFont() )
543 if(!d
->font
.family().isNull())
544 fontstr
+=QLatin1String("font-family: ")+d
->font
.family()+QLatin1String("; ");
546 fontstr
+=QLatin1String("font-style: italic; ");
547 if(d
->font
.strikeOut())
548 fontstr
+=QLatin1String("text-decoration: line-through; ");
549 if(d
->font
.underline())
550 fontstr
+=QLatin1String("text-decoration: underline; ");
552 fontstr
+=QLatin1String("font-weight: bold;");
554 styleAttribute
+= fontstr
;
557 styleAttribute
+= QString::fromUtf8("\"");
559 return styleAttribute
;
562 // prime candidate for removal
564 QString
Message::decodeString( const QByteArray
&message
, const QTextCodec
*providedCodec
, bool *success
)
567 Note to everyone. This function is not the most efficient, that is for sure.
568 However, it *is* the only way we can be guaranteed that a given string is
575 // Avoid heavy codec tests on empty message.
576 if( message
.isEmpty() )
577 return QString::fromAscii( message
);
579 //Check first 128 chars
580 int charsToCheck
= message
.length();
581 charsToCheck
= 128 > charsToCheck
? charsToCheck
: 128;
584 #warning Rewrite the following code: heuristicContentMatch() do not existe anymore.
586 //They are providing a possible codec. Check if it is valid
587 // if( providedCodec && providedCodec->heuristicContentMatch( message, charsToCheck ) >= charsToCheck )
589 //All chars decodable.
590 return providedCodec
->toUnicode( message
);
593 //NOTE see KEncodingDetector@kdecore
595 if( KStringHandler::isUtf8(message
) )
597 //We have a UTF string almost for sure. At least we know it will be decoded.
598 return QString::fromUtf8( message
);
601 //Try codecForContent - exact match
602 QTextCodec *testCodec = QTextCodec::codecForContent(message, charsToCheck);
603 if( testCodec && testCodec->heuristicContentMatch( message, charsToCheck ) >= charsToCheck )
605 //All chars decodable.
606 return testCodec->toUnicode( message );
609 kWarning(14000) << "Unable to decode string using provided codec(s), taking best guesses!";
613 //We don't have any clues here.
616 testCodec = QTextCodec::codecForLocale();
617 if( testCodec && testCodec->heuristicContentMatch( message, charsToCheck ) >= charsToCheck )
619 //All chars decodable.
620 kDebug(14000) << "Using locale's codec";
621 return testCodec->toUnicode( message );
625 testCodec = QTextCodec::codecForMib(4);
626 if( testCodec && testCodec->heuristicContentMatch( message, charsToCheck ) >= charsToCheck )
628 //All chars decodable.
629 kDebug(14000) << "Using latin1";
630 return testCodec->toUnicode( message );
633 kDebug(14000) << "Using latin1 and cleaning string";
634 //No codec decoded. Just decode latin1, and clean out any junk.
635 QString result = QLatin1String( message );
636 const uint length = message.length();
637 for( uint i = 0; i < length; ++i )
639 if( !result[i].isPrint() )
649 QStringList
Message::classes() const
654 void Message::addClass(const QString
& classe
)
656 d
->classes
.append(classe
);
659 void Message::setClasses(const QStringList
& classes
)
661 d
->classes
= classes
;